├── README.md ├── terminal ├── terminal.go ├── terminal_test.go ├── termios.go ├── termios_darwin_amd64.go ├── termios_freebsd_amd64.go ├── termios_linux_amd64.go └── util.go └── test.go /README.md: -------------------------------------------------------------------------------- 1 | # pseudo-terminal-go 2 | 3 | ## Install 4 | ``` 5 | go get github.com/carmark/pseudo-terminal-go/terminal 6 | ``` 7 | 8 | ## Run a test 9 | ``` 10 | go build test.go 11 | ./test 12 | ``` 13 | -------------------------------------------------------------------------------- /terminal/terminal.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package terminal 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "os" 11 | "sync" 12 | ) 13 | 14 | func max(i, j int) int { 15 | if i > j { 16 | return i 17 | } 18 | return j 19 | } 20 | 21 | func min(i, j int) int { 22 | if i < j { 23 | return i 24 | } 25 | return j 26 | } 27 | 28 | // historyIdxValue returns an index into a valid range of history 29 | func historyIdxValue(idx int, history [][]byte) int { 30 | out := idx 31 | out = min(len(history), out) 32 | out = max(0, out) 33 | return out 34 | } 35 | 36 | // EscapeCodes contains escape sequences that can be written to the terminal in 37 | // order to achieve different styles of text. 38 | type EscapeCodes struct { 39 | // Foreground colors 40 | Black, Red, Green, Yellow, Blue, Magenta, Cyan, White []byte 41 | 42 | // Reset all attributes 43 | Reset []byte 44 | } 45 | 46 | var vt100EscapeCodes = EscapeCodes{ 47 | Black: []byte{KeyEscape, '[', '3', '0', 'm'}, 48 | Red: []byte{KeyEscape, '[', '3', '1', 'm'}, 49 | Green: []byte{KeyEscape, '[', '3', '2', 'm'}, 50 | Yellow: []byte{KeyEscape, '[', '3', '3', 'm'}, 51 | Blue: []byte{KeyEscape, '[', '3', '4', 'm'}, 52 | Magenta: []byte{KeyEscape, '[', '3', '5', 'm'}, 53 | Cyan: []byte{KeyEscape, '[', '3', '6', 'm'}, 54 | White: []byte{KeyEscape, '[', '3', '7', 'm'}, 55 | 56 | Reset: []byte{KeyEscape, '[', '0', 'm'}, 57 | } 58 | 59 | // Terminal contains the state for running a VT100 terminal that is capable of 60 | // reading lines of input. 61 | type Terminal struct { 62 | // AutoCompleteCallback, if non-null, is called for each keypress 63 | // with the full input line and the current position of the cursor. 64 | // If it returns a nil newLine, the key press is processed normally. 65 | // Otherwise it returns a replacement line and the new cursor position. 66 | AutoCompleteCallback func(line []byte, pos, key int) (newLine []byte, newPos int) 67 | 68 | // Escape contains a pointer to the escape codes for this terminal. 69 | // It's always a valid pointer, although the escape codes themselves 70 | // may be empty if the terminal doesn't support them. 71 | Escape *EscapeCodes 72 | 73 | // lock protects the terminal and the state in this object from 74 | // concurrent processing of a key press and a Write() call. 75 | lock sync.Mutex 76 | 77 | c io.ReadWriter 78 | prompt string 79 | 80 | // line is the current line being entered. 81 | line []byte 82 | // history is a buffer of previously entered lines 83 | history [][]byte 84 | // index into the history buffer (for use in the handleKey(KeyUp) function) 85 | historyIdx int 86 | // pos is the logical position of the cursor in line 87 | pos int 88 | // echo is true if local echo is enabled 89 | echo bool 90 | 91 | // cursorX contains the current X value of the cursor where the left 92 | // edge is 0. cursorY contains the row number where the first row of 93 | // the current line is 0. 94 | cursorX, cursorY int 95 | // maxLine is the greatest value of cursorY so far. 96 | maxLine int 97 | 98 | termWidth, termHeight int 99 | 100 | // outBuf contains the terminal data to be sent. 101 | outBuf []byte 102 | // remainder contains the remainder of any partial key sequences after 103 | // a read. It aliases into inBuf. 104 | remainder []byte 105 | inBuf [256]byte 106 | } 107 | 108 | // NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is 109 | // a local terminal, that terminal must first have been put into raw mode. 110 | // prompt is a string that is written at the start of each input line (i.e. 111 | // "> "). 112 | func NewTerminal(c io.ReadWriter, prompt string) *Terminal { 113 | return &Terminal{ 114 | Escape: &vt100EscapeCodes, 115 | c: c, 116 | prompt: prompt, 117 | history: make([][]byte, 0, 100), 118 | historyIdx: -1, 119 | termWidth: 80, 120 | termHeight: 24, 121 | echo: true, 122 | } 123 | } 124 | 125 | const ( 126 | KeyCtrlC = 3 127 | KeyCtrlD = 4 128 | KeyEnter = '\r' 129 | KeyEscape = 27 130 | KeyBackspace = 127 131 | KeyUnknown = 256 + iota 132 | KeyLeft 133 | KeyUp 134 | KeyRight 135 | KeyDown 136 | KeyAltLeft 137 | KeyAltRight 138 | ) 139 | 140 | // bytesToKey tries to parse a key sequence from b. If successful, it returns 141 | // the key and the remainder of the input. Otherwise it returns -1. 142 | func bytesToKey(b []byte) (int, []byte) { 143 | if len(b) == 0 { 144 | return -1, nil 145 | } 146 | 147 | if b[0] != KeyEscape { 148 | return int(b[0]), b[1:] 149 | } 150 | 151 | if len(b) >= 3 && b[0] == KeyEscape && b[1] == '[' { 152 | switch b[2] { 153 | case 'A': 154 | return KeyUp, b[3:] 155 | case 'B': 156 | return KeyDown, b[3:] 157 | case 'C': 158 | return KeyRight, b[3:] 159 | case 'D': 160 | return KeyLeft, b[3:] 161 | } 162 | } 163 | 164 | if len(b) >= 6 && 165 | b[0] == KeyEscape && 166 | b[1] == '[' && 167 | b[2] == '1' && 168 | b[3] == ';' && 169 | b[4] == '3' { 170 | switch b[5] { 171 | case 'C': 172 | return KeyAltRight, b[6:] 173 | case 'D': 174 | return KeyAltLeft, b[6:] 175 | } 176 | } 177 | 178 | // If we get here then we have a key that we don't recognise, or a 179 | // partial sequence. It's not clear how one should find the end of a 180 | // sequence without knowing them all, but it seems that [a-zA-Z] only 181 | // appears at the end of a sequence. 182 | for i, c := range b[0:] { 183 | if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' { 184 | return KeyUnknown, b[i+1:] 185 | } 186 | } 187 | 188 | return -1, b 189 | } 190 | 191 | // queue appends data to the end of t.outBuf 192 | func (t *Terminal) queue(data []byte) { 193 | t.outBuf = append(t.outBuf, data...) 194 | } 195 | 196 | var eraseUnderCursor = []byte{' ', KeyEscape, '[', 'D'} 197 | var space = []byte{' '} 198 | 199 | func isPrintable(key int) bool { 200 | return key >= 32 && key < 127 201 | } 202 | 203 | // moveCursorToPos appends data to t.outBuf which will move the cursor to the 204 | // given, logical position in the text. 205 | func (t *Terminal) moveCursorToPos(pos int) { 206 | if !t.echo { 207 | return 208 | } 209 | 210 | x := len(t.prompt) + pos 211 | y := x / t.termWidth 212 | x = x % t.termWidth 213 | 214 | up := 0 215 | if y < t.cursorY { 216 | up = t.cursorY - y 217 | } 218 | 219 | down := 0 220 | if y > t.cursorY { 221 | down = y - t.cursorY 222 | } 223 | 224 | left := 0 225 | if x < t.cursorX { 226 | left = t.cursorX - x 227 | } 228 | 229 | right := 0 230 | if x > t.cursorX { 231 | right = x - t.cursorX 232 | } 233 | 234 | t.cursorX = x 235 | t.cursorY = y 236 | t.move(up, down, left, right) 237 | } 238 | 239 | func (t *Terminal) move(up, down, left, right int) { 240 | movement := make([]byte, 3*(up+down+left+right)) 241 | m := movement 242 | for i := 0; i < up; i++ { 243 | m[0] = KeyEscape 244 | m[1] = '[' 245 | m[2] = 'A' 246 | m = m[3:] 247 | } 248 | for i := 0; i < down; i++ { 249 | m[0] = KeyEscape 250 | m[1] = '[' 251 | m[2] = 'B' 252 | m = m[3:] 253 | } 254 | for i := 0; i < left; i++ { 255 | m[0] = KeyEscape 256 | m[1] = '[' 257 | m[2] = 'D' 258 | m = m[3:] 259 | } 260 | for i := 0; i < right; i++ { 261 | m[0] = KeyEscape 262 | m[1] = '[' 263 | m[2] = 'C' 264 | m = m[3:] 265 | } 266 | 267 | t.queue(movement) 268 | } 269 | 270 | func (t *Terminal) clearLineToRight() { 271 | op := []byte{KeyEscape, '[', 'K'} 272 | t.queue(op) 273 | } 274 | 275 | const maxLineLength = 4096 276 | 277 | // handleKey processes the given key and, optionally, returns a line of text 278 | // that the user has entered. 279 | func (t *Terminal) handleKey(key int) (line string, ok bool) { 280 | switch key { 281 | case KeyBackspace: 282 | if t.pos == 0 { 283 | return 284 | } 285 | t.pos-- 286 | t.moveCursorToPos(t.pos) 287 | 288 | copy(t.line[t.pos:], t.line[1+t.pos:]) 289 | t.line = t.line[:len(t.line)-1] 290 | if t.echo { 291 | t.writeLine(t.line[t.pos:]) 292 | } 293 | t.queue(eraseUnderCursor) 294 | t.moveCursorToPos(t.pos) 295 | case KeyAltLeft: 296 | // move left by a word. 297 | if t.pos == 0 { 298 | return 299 | } 300 | t.pos-- 301 | for t.pos > 0 { 302 | if t.line[t.pos] != ' ' { 303 | break 304 | } 305 | t.pos-- 306 | } 307 | for t.pos > 0 { 308 | if t.line[t.pos] == ' ' { 309 | t.pos++ 310 | break 311 | } 312 | t.pos-- 313 | } 314 | t.moveCursorToPos(t.pos) 315 | case KeyAltRight: 316 | // move right by a word. 317 | for t.pos < len(t.line) { 318 | if t.line[t.pos] == ' ' { 319 | break 320 | } 321 | t.pos++ 322 | } 323 | for t.pos < len(t.line) { 324 | if t.line[t.pos] != ' ' { 325 | break 326 | } 327 | t.pos++ 328 | } 329 | t.moveCursorToPos(t.pos) 330 | case KeyLeft: 331 | if t.pos == 0 { 332 | return 333 | } 334 | t.pos-- 335 | t.moveCursorToPos(t.pos) 336 | case KeyRight: 337 | if t.pos == len(t.line) { 338 | return 339 | } 340 | t.pos++ 341 | t.moveCursorToPos(t.pos) 342 | case KeyUp: 343 | if len(t.history) == 0 { 344 | return 345 | } 346 | t.historyIdx-- 347 | t.historyIdx = historyIdxValue(t.historyIdx, t.history) 348 | 349 | h := t.history[t.historyIdx] 350 | newLine := make([]byte, len(h)) 351 | copy(newLine, h) 352 | newPos := len(newLine) 353 | if t.echo { 354 | t.moveCursorToPos(0) 355 | t.writeLine(newLine) 356 | for i := len(newLine); i < len(t.line); i++ { 357 | t.writeLine(space) 358 | } 359 | t.moveCursorToPos(newPos) 360 | } 361 | t.line = newLine 362 | t.pos = newPos 363 | return 364 | 365 | case KeyDown: 366 | if len(t.history) == 0 { 367 | return 368 | } 369 | newPos := 0 370 | newLine := []byte{} 371 | t.historyIdx++ 372 | if t.historyIdx >= len(t.history) { 373 | t.historyIdx = len(t.history) 374 | } else { 375 | t.historyIdx = historyIdxValue(t.historyIdx, t.history) 376 | h := t.history[t.historyIdx] 377 | newLine = make([]byte, len(h)) 378 | copy(newLine, h) 379 | newPos = len(newLine) 380 | // fmt.Println("in") 381 | } 382 | if t.echo { 383 | t.moveCursorToPos(0) 384 | t.writeLine(newLine) 385 | for i := len(newLine); i < len(t.line); i++ { 386 | t.writeLine(space) 387 | } 388 | t.moveCursorToPos(newPos) 389 | } 390 | t.line = newLine 391 | t.pos = newPos 392 | return 393 | 394 | case KeyEnter: 395 | t.moveCursorToPos(len(t.line)) 396 | t.queue([]byte("\r\n")) 397 | line = string(t.line) 398 | ok = true 399 | t.line = t.line[:0] 400 | t.pos = 0 401 | t.cursorX = 0 402 | t.cursorY = 0 403 | t.maxLine = 0 404 | t.historyIdx = len(t.history) + 1 405 | case KeyCtrlD: 406 | // add 'exit' to the end of the line 407 | ok = true 408 | if len(t.line) == 0 { 409 | if len(t.line) == maxLineLength { 410 | return 411 | } 412 | if len(t.line) == cap(t.line) { 413 | newLine := make([]byte, len(t.line), 2*(2+len(t.line))) 414 | copy(newLine, t.line) 415 | t.line = newLine 416 | } 417 | t.line = t.line[:len(t.line)+4] 418 | copy(t.line[t.pos+4:], t.line[t.pos:]) 419 | t.line[t.pos] = byte('e') 420 | if t.echo { 421 | t.writeLine(t.line[t.pos:]) 422 | } 423 | t.pos++ 424 | t.moveCursorToPos(t.pos) 425 | t.line[t.pos] = byte('x') 426 | if t.echo { 427 | t.writeLine(t.line[t.pos:]) 428 | } 429 | t.pos++ 430 | t.moveCursorToPos(t.pos) 431 | t.line[t.pos] = byte('i') 432 | if t.echo { 433 | t.writeLine(t.line[t.pos:]) 434 | } 435 | t.pos++ 436 | t.moveCursorToPos(t.pos) 437 | t.line[t.pos] = byte('t') 438 | if t.echo { 439 | t.writeLine(t.line[t.pos:]) 440 | } 441 | t.pos++ 442 | t.moveCursorToPos(t.pos) 443 | } 444 | case KeyCtrlC: 445 | // add '^C' to the end of the line 446 | if len(t.line) == maxLineLength { 447 | return 448 | } 449 | newLine := make([]byte, len(t.line), 2*(2+len(t.line))) 450 | copy(newLine, t.line) 451 | t.line = newLine 452 | t.line = t.line[:len(t.line)+3] 453 | copy(t.line[t.pos+3:], t.line[t.pos:]) 454 | t.line[t.pos] = byte('^') 455 | t.pos++ 456 | t.line[t.pos] = byte('C') 457 | if t.echo { 458 | t.writeLine(t.line[t.pos-1:]) 459 | } 460 | t.pos ++ 461 | t.moveCursorToPos(t.pos) 462 | t.queue([]byte("\r\n")) 463 | t.line = make([]byte, 0, 0) 464 | t.pos = 0 465 | t.cursorX = 0 466 | t.cursorY = 0 467 | 468 | default: 469 | if t.AutoCompleteCallback != nil { 470 | t.lock.Unlock() 471 | newLine, newPos := t.AutoCompleteCallback(t.line, t.pos, key) 472 | t.lock.Lock() 473 | 474 | if newLine != nil { 475 | if t.echo { 476 | t.moveCursorToPos(0) 477 | t.writeLine(newLine) 478 | for i := len(newLine); i < len(t.line); i++ { 479 | t.writeLine(space) 480 | } 481 | t.moveCursorToPos(newPos) 482 | } 483 | t.line = newLine 484 | t.pos = newPos 485 | return 486 | } 487 | } 488 | if !isPrintable(key) { 489 | return 490 | } 491 | if len(t.line) == maxLineLength { 492 | return 493 | } 494 | if len(t.line) == cap(t.line) { 495 | newLine := make([]byte, len(t.line), 2*(1+len(t.line))) 496 | copy(newLine, t.line) 497 | t.line = newLine 498 | } 499 | t.line = t.line[:len(t.line)+1] 500 | copy(t.line[t.pos+1:], t.line[t.pos:]) 501 | t.line[t.pos] = byte(key) 502 | if t.echo { 503 | t.writeLine(t.line[t.pos:]) 504 | } 505 | t.pos++ 506 | t.moveCursorToPos(t.pos) 507 | } 508 | return 509 | } 510 | 511 | func (t *Terminal) writeLine(line []byte) { 512 | for len(line) != 0 { 513 | remainingOnLine := t.termWidth - t.cursorX 514 | todo := len(line) 515 | if todo > remainingOnLine { 516 | todo = remainingOnLine 517 | } 518 | t.queue(line[:todo]) 519 | t.cursorX += todo 520 | line = line[todo:] 521 | 522 | if t.cursorX == t.termWidth { 523 | t.cursorX = 0 524 | t.cursorY++ 525 | if t.cursorY > t.maxLine { 526 | t.maxLine = t.cursorY 527 | } 528 | } 529 | } 530 | } 531 | 532 | func (t *Terminal) Write(buf []byte) (n int, err error) { 533 | t.lock.Lock() 534 | defer t.lock.Unlock() 535 | 536 | if t.cursorX == 0 && t.cursorY == 0 { 537 | // This is the easy case: there's nothing on the screen that we 538 | // have to move out of the way. 539 | return t.c.Write(buf) 540 | } 541 | 542 | // We have a prompt and possibly user input on the screen. We 543 | // have to clear it first. 544 | t.move(0 /* up */, 0 /* down */, t.cursorX /* left */, 0 /* right */) 545 | t.cursorX = 0 546 | t.clearLineToRight() 547 | 548 | for t.cursorY > 0 { 549 | t.move(1 /* up */, 0, 0, 0) 550 | t.cursorY-- 551 | t.clearLineToRight() 552 | } 553 | 554 | if _, err = t.c.Write(t.outBuf); err != nil { 555 | return 556 | } 557 | t.outBuf = t.outBuf[:0] 558 | 559 | if n, err = t.c.Write(buf); err != nil { 560 | return 561 | } 562 | 563 | t.queue([]byte(t.prompt)) 564 | chars := len(t.prompt) 565 | if t.echo { 566 | t.queue(t.line) 567 | chars += len(t.line) 568 | } 569 | t.cursorX = chars % t.termWidth 570 | t.cursorY = chars / t.termWidth 571 | t.moveCursorToPos(t.pos) 572 | 573 | if _, err = t.c.Write(t.outBuf); err != nil { 574 | return 575 | } 576 | t.outBuf = t.outBuf[:0] 577 | return 578 | } 579 | 580 | // ReadPassword temporarily changes the prompt and reads a password, without 581 | // echo, from the terminal. 582 | func (t *Terminal) ReadPassword(prompt string) (line string, err error) { 583 | t.lock.Lock() 584 | defer t.lock.Unlock() 585 | 586 | oldPrompt := t.prompt 587 | t.prompt = prompt 588 | t.echo = false 589 | 590 | line, err = t.readLine() 591 | 592 | t.prompt = oldPrompt 593 | t.echo = true 594 | 595 | return 596 | } 597 | 598 | // ReadLine returns a line of input from the terminal. 599 | func (t *Terminal) ReadLine() (line string, err error) { 600 | t.lock.Lock() 601 | defer t.lock.Unlock() 602 | 603 | return t.readLine() 604 | } 605 | 606 | func (t *Terminal) readLine() (line string, err error) { 607 | // t.lock must be held at this point 608 | 609 | if t.cursorX == 0 && t.cursorY == 0 { 610 | t.writeLine([]byte(t.prompt)) 611 | t.c.Write(t.outBuf) 612 | t.outBuf = t.outBuf[:0] 613 | } 614 | 615 | for { 616 | rest := t.remainder 617 | lineOk := false 618 | for !lineOk { 619 | var key int 620 | key, rest = bytesToKey(rest) 621 | if key < 0 { 622 | break 623 | } 624 | 625 | line, lineOk = t.handleKey(key) 626 | if key == KeyCtrlD && lineOk == true { 627 | return "", io.EOF 628 | } 629 | if key == KeyCtrlC { 630 | t.remainder = nil 631 | return "^C", fmt.Errorf("control-c break") 632 | } 633 | } 634 | if len(rest) > 0 { 635 | n := copy(t.inBuf[:], rest) 636 | t.remainder = t.inBuf[:n] 637 | } else { 638 | t.remainder = nil 639 | } 640 | t.c.Write(t.outBuf) 641 | t.outBuf = t.outBuf[:0] 642 | if lineOk { 643 | if t.echo { //&& len(line) > 0 { 644 | // don't put passwords into history... 645 | b := []byte(line) 646 | h := make([]byte, len(b)) 647 | copy(h, b) 648 | t.history = append(t.history, h) 649 | } 650 | return 651 | } 652 | 653 | // t.remainder is a slice at the beginning of t.inBuf 654 | // containing a partial key sequence 655 | readBuf := t.inBuf[len(t.remainder):] 656 | var n int 657 | 658 | t.lock.Unlock() 659 | n, err = t.c.Read(readBuf) 660 | t.lock.Lock() 661 | 662 | if err != nil { 663 | return "", err 664 | } 665 | 666 | t.remainder = t.inBuf[:n+len(t.remainder)] 667 | } 668 | panic("unreachable") 669 | } 670 | 671 | // SetPrompt sets the prompt to be used when reading subsequent lines. 672 | func (t *Terminal) SetPrompt(prompt string) { 673 | t.lock.Lock() 674 | defer t.lock.Unlock() 675 | 676 | t.prompt = prompt 677 | } 678 | 679 | func (t *Terminal) SetSize(width, height int) { 680 | t.lock.Lock() 681 | defer t.lock.Unlock() 682 | 683 | t.termWidth, t.termHeight = width, height 684 | } 685 | 686 | func (t *Terminal) SetHistory(h []string) { 687 | // t.history = make([][]byte, len(h)) 688 | // for i := range h { 689 | // t.history[i] = []byte(h[i]) 690 | // } 691 | // // t.historyIdx = len(h) 692 | } 693 | 694 | func (t *Terminal) GetHistory() (h []string) { 695 | // h = make([]string, len(t.history)) 696 | // for i := range t.history { 697 | // h[i] = string(t.history[i]) 698 | // } 699 | return 700 | } 701 | 702 | type shell struct { 703 | r io.Reader 704 | w io.Writer 705 | } 706 | 707 | func (sh *shell) Read(data []byte) (n int, err error) { 708 | return sh.r.Read(data) 709 | } 710 | 711 | func (sh *shell) Write(data []byte) (n int, err error) { 712 | return sh.w.Write(data) 713 | } 714 | 715 | var oldState *State 716 | 717 | func (t *Terminal) ReleaseFromStdInOut() { // doesn't really need a receiver, but maybe oldState can be part of term one day 718 | fd := int(os.Stdin.Fd()) 719 | Restore(fd, oldState) 720 | } 721 | 722 | func NewWithStdInOut() (term *Terminal, err error) { 723 | fd := int(os.Stdin.Fd()) 724 | oldState, err = MakeRaw(fd) 725 | if err != nil { 726 | panic(err) 727 | } 728 | sh := &shell{r: os.Stdin, w: os.Stdout} 729 | term = NewTerminal(sh, "") 730 | return 731 | } 732 | -------------------------------------------------------------------------------- /terminal/terminal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package terminal 6 | 7 | import ( 8 | "io" 9 | "testing" 10 | ) 11 | 12 | type MockTerminal struct { 13 | toSend []byte 14 | bytesPerRead int 15 | received []byte 16 | } 17 | 18 | func (c *MockTerminal) Read(data []byte) (n int, err error) { 19 | n = len(data) 20 | if n == 0 { 21 | return 22 | } 23 | if n > len(c.toSend) { 24 | n = len(c.toSend) 25 | } 26 | if n == 0 { 27 | return 0, io.EOF 28 | } 29 | if c.bytesPerRead > 0 && n > c.bytesPerRead { 30 | n = c.bytesPerRead 31 | } 32 | copy(data, c.toSend[:n]) 33 | c.toSend = c.toSend[n:] 34 | return 35 | } 36 | 37 | func (c *MockTerminal) Write(data []byte) (n int, err error) { 38 | c.received = append(c.received, data...) 39 | return len(data), nil 40 | } 41 | 42 | func TestClose(t *testing.T) { 43 | c := &MockTerminal{} 44 | ss := NewTerminal(c, "> ") 45 | line, err := ss.ReadLine() 46 | if line != "" { 47 | t.Errorf("Expected empty line but got: %s", line) 48 | } 49 | if err != io.EOF { 50 | t.Errorf("Error should have been EOF but got: %s", err) 51 | } 52 | } 53 | 54 | var keyPressTests = []struct { 55 | in string 56 | line string 57 | err error 58 | }{ 59 | { 60 | "", 61 | "", 62 | io.EOF, 63 | }, 64 | { 65 | "\r", 66 | "", 67 | nil, 68 | }, 69 | { 70 | "foo\r", 71 | "foo", 72 | nil, 73 | }, 74 | { 75 | "a\x1b[Cb\r", // right 76 | "ab", 77 | nil, 78 | }, 79 | { 80 | "a\x1b[Db\r", // left 81 | "ba", 82 | nil, 83 | }, 84 | { 85 | "a\177b\r", // backspace 86 | "b", 87 | nil, 88 | }, 89 | } 90 | 91 | func TestKeyPresses(t *testing.T) { 92 | for i, test := range keyPressTests { 93 | for j := 0; j < len(test.in); j++ { 94 | c := &MockTerminal{ 95 | toSend: []byte(test.in), 96 | bytesPerRead: j, 97 | } 98 | ss := NewTerminal(c, "> ") 99 | line, err := ss.ReadLine() 100 | if line != test.line { 101 | t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line) 102 | break 103 | } 104 | if err != test.err { 105 | t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err) 106 | break 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /terminal/termios.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package terminal 4 | 5 | /* 6 | #include 7 | #include 8 | */ 9 | import "C" 10 | 11 | type syscall_Termios C.struct_termios 12 | 13 | const ( 14 | syscall_IGNBRK = C.IGNBRK 15 | syscall_BRKINT = C.BRKINT 16 | syscall_PARMRK = C.PARMRK 17 | syscall_ISTRIP = C.ISTRIP 18 | syscall_INLCR = C.INLCR 19 | syscall_IGNCR = C.IGNCR 20 | syscall_ICRNL = C.ICRNL 21 | syscall_IXON = C.IXON 22 | syscall_IXOFF = C.IXOFF 23 | syscall_OPOST = C.OPOST 24 | syscall_ECHO = C.ECHO 25 | syscall_ECHONL = C.ECHONL 26 | syscall_ICANON = C.ICANON 27 | syscall_ISIG = C.ISIG 28 | syscall_IEXTEN = C.IEXTEN 29 | syscall_CSIZE = C.CSIZE 30 | syscall_PARENB = C.PARENB 31 | syscall_CS8 = C.CS8 32 | syscall_VMIN = C.VMIN 33 | syscall_VTIME = C.VTIME 34 | 35 | // on darwin change these to (on *bsd too?): 36 | // C.TIOCGETA 37 | // C.TIOCSETA 38 | syscall_TCGETS = C.TIOCGETA 39 | syscall_TCSETS = C.TIOCSETA 40 | ) 41 | -------------------------------------------------------------------------------- /terminal/termios_darwin_amd64.go: -------------------------------------------------------------------------------- 1 | // Created by cgo -godefs - DO NOT EDIT 2 | // cgo -godefs termios.go 3 | 4 | package terminal 5 | 6 | type syscall_Termios struct { 7 | Iflag uint64 8 | Oflag uint64 9 | Cflag uint64 10 | Lflag uint64 11 | Cc [20]uint8 12 | Pad_cgo_0 [4]byte 13 | Ispeed uint64 14 | Ospeed uint64 15 | } 16 | 17 | const ( 18 | syscall_IGNBRK = 0x1 19 | syscall_BRKINT = 0x2 20 | syscall_PARMRK = 0x8 21 | syscall_ISTRIP = 0x20 22 | syscall_INLCR = 0x40 23 | syscall_IGNCR = 0x80 24 | syscall_ICRNL = 0x100 25 | syscall_IXON = 0x200 26 | syscall_IXOFF = 0x400 27 | syscall_OPOST = 0x1 28 | syscall_ECHO = 0x8 29 | syscall_ECHONL = 0x10 30 | syscall_ICANON = 0x100 31 | syscall_ISIG = 0x80 32 | syscall_IEXTEN = 0x400 33 | syscall_CSIZE = 0x300 34 | syscall_PARENB = 0x1000 35 | syscall_CS8 = 0x300 36 | syscall_VMIN = 0x10 37 | syscall_VTIME = 0x11 38 | 39 | syscall_TCGETS = 0x40487413 40 | syscall_TCSETS = 0x80487414 41 | ) 42 | -------------------------------------------------------------------------------- /terminal/termios_freebsd_amd64.go: -------------------------------------------------------------------------------- 1 | // Created by cgo -godefs - DO NOT EDIT 2 | // /usr/local/go/bin/cgo -godefs termios.go 3 | 4 | package terminal 5 | 6 | type syscall_Termios struct { 7 | Iflag uint32 8 | Oflag uint32 9 | Cflag uint32 10 | Lflag uint32 11 | Cc [20]uint8 12 | Ispeed uint32 13 | Ospeed uint32 14 | } 15 | 16 | const ( 17 | syscall_IGNBRK = 0x1 18 | syscall_BRKINT = 0x2 19 | syscall_PARMRK = 0x8 20 | syscall_ISTRIP = 0x20 21 | syscall_INLCR = 0x40 22 | syscall_IGNCR = 0x80 23 | syscall_ICRNL = 0x100 24 | syscall_IXON = 0x200 25 | syscall_IXOFF = 0x400 26 | syscall_OPOST = 0x1 27 | syscall_ECHO = 0x8 28 | syscall_ECHONL = 0x10 29 | syscall_ICANON = 0x100 30 | syscall_ISIG = 0x80 31 | syscall_IEXTEN = 0x400 32 | syscall_CSIZE = 0x300 33 | syscall_PARENB = 0x1000 34 | syscall_CS8 = 0x300 35 | syscall_VMIN = 0x10 36 | syscall_VTIME = 0x11 37 | 38 | syscall_TCGETS = 0x402c7413 39 | syscall_TCSETS = 0x802c7414 40 | ) 41 | -------------------------------------------------------------------------------- /terminal/termios_linux_amd64.go: -------------------------------------------------------------------------------- 1 | // Created by cgo -godefs - DO NOT EDIT 2 | // cgo -godefs termios.go 3 | 4 | package terminal 5 | 6 | type syscall_Termios struct { 7 | Iflag uint32 8 | Oflag uint32 9 | Cflag uint32 10 | Lflag uint32 11 | Line uint8 12 | Cc [32]uint8 13 | Pad_cgo_0 [3]byte 14 | Ispeed uint32 15 | Ospeed uint32 16 | } 17 | 18 | const ( 19 | syscall_IGNBRK = 0x1 20 | syscall_BRKINT = 0x2 21 | syscall_PARMRK = 0x8 22 | syscall_ISTRIP = 0x20 23 | syscall_INLCR = 0x40 24 | syscall_IGNCR = 0x80 25 | syscall_ICRNL = 0x100 26 | syscall_IXON = 0x400 27 | syscall_IXOFF = 0x1000 28 | syscall_OPOST = 0x1 29 | syscall_ECHO = 0x8 30 | syscall_ECHONL = 0x40 31 | syscall_ICANON = 0x2 32 | syscall_ISIG = 0x1 33 | syscall_IEXTEN = 0x8000 34 | syscall_CSIZE = 0x30 35 | syscall_PARENB = 0x100 36 | syscall_CS8 = 0x30 37 | syscall_VMIN = 0x6 38 | syscall_VTIME = 0x5 39 | 40 | syscall_TCGETS = 0x5401 41 | syscall_TCSETS = 0x5402 42 | ) 43 | -------------------------------------------------------------------------------- /terminal/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build linux darwin freebsd 6 | 7 | // Package terminal provides support functions for dealing with terminals, as 8 | // commonly found on UNIX systems. 9 | // 10 | // Putting a terminal into raw mode is the most common requirement: 11 | // 12 | // oldState, err := terminal.MakeRaw(0) 13 | // if err != nil { 14 | // panic(err) 15 | // } 16 | // defer terminal.Restore(0, oldState) 17 | package terminal 18 | 19 | import ( 20 | "io" 21 | "syscall" 22 | "unsafe" 23 | ) 24 | 25 | // State contains the state of a terminal. 26 | type State struct { 27 | termios syscall_Termios 28 | } 29 | 30 | // IsTerminal returns true if the given file descriptor is a terminal. 31 | func IsTerminal(fd int) bool { 32 | var termios syscall_Termios 33 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall_TCGETS), uintptr(unsafe.Pointer(&termios)), 0, 0, 0) 34 | return err == 0 35 | } 36 | 37 | // MakeRaw put the terminal connected to the given file descriptor into raw 38 | // mode and returns the previous state of the terminal so that it can be 39 | // restored. 40 | func MakeRaw(fd int) (*State, error) { 41 | var oldState State 42 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall_TCGETS), uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { 43 | return nil, err 44 | } 45 | 46 | newState := oldState.termios 47 | newState.Iflag &^= syscall_ISTRIP | syscall_INLCR | syscall_ICRNL | syscall_IGNCR | syscall_IXON | syscall_IXOFF 48 | newState.Lflag &^= syscall.ECHO | syscall_ICANON | syscall_ISIG 49 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall_TCSETS), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { 50 | return nil, err 51 | } 52 | 53 | return &oldState, nil 54 | } 55 | 56 | // Restore restores the terminal connected to the given file descriptor to a 57 | // previous state. 58 | func Restore(fd int, state *State) error { 59 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall_TCSETS), uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) 60 | return err 61 | } 62 | 63 | // GetSize returns the dimensions of the given terminal. 64 | func GetSize(fd int) (width, height int, err error) { 65 | var dimensions [4]uint16 66 | 67 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0); err != 0 { 68 | return -1, -1, err 69 | } 70 | return int(dimensions[1]), int(dimensions[0]), nil 71 | } 72 | 73 | // ReadPassword reads a line of input from a terminal without local echo. This 74 | // is commonly used for inputting passwords and other sensitive data. The slice 75 | // returned does not include the \n. 76 | func ReadPassword(fd int) ([]byte, error) { 77 | var oldState syscall_Termios 78 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall_TCGETS), uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); err != 0 { 79 | return nil, err 80 | } 81 | 82 | newState := oldState 83 | newState.Lflag &^= syscall.ECHO 84 | if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall_TCSETS), uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { 85 | return nil, err 86 | } 87 | 88 | defer func() { 89 | syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall_TCSETS), uintptr(unsafe.Pointer(&oldState)), 0, 0, 0) 90 | }() 91 | 92 | var buf [16]byte 93 | var ret []byte 94 | for { 95 | n, err := syscall.Read(fd, buf[:]) 96 | if err != nil { 97 | return nil, err 98 | } 99 | if n == 0 { 100 | if len(ret) == 0 { 101 | return nil, io.EOF 102 | } 103 | break 104 | } 105 | if buf[n-1] == '\n' { 106 | n-- 107 | } 108 | ret = append(ret, buf[:n]...) 109 | if n < len(buf) { 110 | break 111 | } 112 | } 113 | 114 | return ret, nil 115 | } 116 | -------------------------------------------------------------------------------- /test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/carmark/pseudo-terminal-go/terminal" 9 | ) 10 | 11 | func main() { 12 | term, err := terminal.NewWithStdInOut() 13 | if err != nil { 14 | panic(err) 15 | } 16 | defer term.ReleaseFromStdInOut() // defer this 17 | fmt.Println("Ctrl-D to break") 18 | term.SetPrompt("root@hello: # ") 19 | line, err:= term.ReadLine() 20 | for { 21 | if err == io.EOF { 22 | term.Write([]byte(line)) 23 | fmt.Println() 24 | return 25 | } 26 | if (err != nil && strings.Contains(err.Error(), "control-c break")) || len(line) == 0{ 27 | line, err = term.ReadLine() 28 | } else { 29 | term.Write([]byte(line+"\r\n")) 30 | line, err = term.ReadLine() 31 | } 32 | } 33 | term.Write([]byte(line)) 34 | } 35 | --------------------------------------------------------------------------------