1// +build windows linux darwin openbsd freebsd netbsd 2 3package liner 4 5import ( 6 "bufio" 7 "container/ring" 8 "errors" 9 "fmt" 10 "io" 11 "os" 12 "strings" 13 "unicode" 14 "unicode/utf8" 15) 16 17type action int 18 19const ( 20 left action = iota 21 right 22 up 23 down 24 home 25 end 26 insert 27 del 28 pageUp 29 pageDown 30 f1 31 f2 32 f3 33 f4 34 f5 35 f6 36 f7 37 f8 38 f9 39 f10 40 f11 41 f12 42 altB 43 altD 44 altF 45 altY 46 shiftTab 47 wordLeft 48 wordRight 49 winch 50 unknown 51) 52 53const ( 54 ctrlA = 1 55 ctrlB = 2 56 ctrlC = 3 57 ctrlD = 4 58 ctrlE = 5 59 ctrlF = 6 60 ctrlG = 7 61 ctrlH = 8 62 tab = 9 63 lf = 10 64 ctrlK = 11 65 ctrlL = 12 66 cr = 13 67 ctrlN = 14 68 ctrlO = 15 69 ctrlP = 16 70 ctrlQ = 17 71 ctrlR = 18 72 ctrlS = 19 73 ctrlT = 20 74 ctrlU = 21 75 ctrlV = 22 76 ctrlW = 23 77 ctrlX = 24 78 ctrlY = 25 79 ctrlZ = 26 80 esc = 27 81 bs = 127 82) 83 84const ( 85 beep = "\a" 86) 87 88type tabDirection int 89 90const ( 91 tabForward tabDirection = iota 92 tabReverse 93) 94 95func (s *State) refresh(prompt []rune, buf []rune, pos int) error { 96 if s.columns == 0 { 97 return ErrInternal 98 } 99 100 s.needRefresh = false 101 if s.multiLineMode { 102 return s.refreshMultiLine(prompt, buf, pos) 103 } 104 return s.refreshSingleLine(prompt, buf, pos) 105} 106 107func (s *State) refreshSingleLine(prompt []rune, buf []rune, pos int) error { 108 s.cursorPos(0) 109 _, err := fmt.Print(string(prompt)) 110 if err != nil { 111 return err 112 } 113 114 pLen := countGlyphs(prompt) 115 bLen := countGlyphs(buf) 116 // on some OS / terminals extra column is needed to place the cursor char 117 if cursorColumn { 118 bLen++ 119 } 120 pos = countGlyphs(buf[:pos]) 121 if pLen+bLen < s.columns { 122 _, err = fmt.Print(string(buf)) 123 s.eraseLine() 124 s.cursorPos(pLen + pos) 125 } else { 126 // Find space available 127 space := s.columns - pLen 128 space-- // space for cursor 129 start := pos - space/2 130 end := start + space 131 if end > bLen { 132 end = bLen 133 start = end - space 134 } 135 if start < 0 { 136 start = 0 137 end = space 138 } 139 pos -= start 140 141 // Leave space for markers 142 if start > 0 { 143 start++ 144 } 145 if end < bLen { 146 end-- 147 } 148 startRune := len(getPrefixGlyphs(buf, start)) 149 line := getPrefixGlyphs(buf[startRune:], end-start) 150 151 // Output 152 if start > 0 { 153 fmt.Print("{") 154 } 155 fmt.Print(string(line)) 156 if end < bLen { 157 fmt.Print("}") 158 } 159 160 // Set cursor position 161 s.eraseLine() 162 s.cursorPos(pLen + pos) 163 } 164 return err 165} 166 167func (s *State) refreshMultiLine(prompt []rune, buf []rune, pos int) error { 168 promptColumns := countMultiLineGlyphs(prompt, s.columns, 0) 169 totalColumns := countMultiLineGlyphs(buf, s.columns, promptColumns) 170 // on some OS / terminals extra column is needed to place the cursor char 171 // if cursorColumn { 172 // totalColumns++ 173 // } 174 175 // it looks like Multiline mode always assume that a cursor need an extra column, 176 // and always emit a newline if we are at the screen end, so no worarounds needed there 177 178 totalRows := (totalColumns + s.columns - 1) / s.columns 179 maxRows := s.maxRows 180 if totalRows > s.maxRows { 181 s.maxRows = totalRows 182 } 183 cursorRows := s.cursorRows 184 if cursorRows == 0 { 185 cursorRows = 1 186 } 187 188 /* First step: clear all the lines used before. To do so start by 189 * going to the last row. */ 190 if maxRows-cursorRows > 0 { 191 s.moveDown(maxRows - cursorRows) 192 } 193 194 /* Now for every row clear it, go up. */ 195 for i := 0; i < maxRows-1; i++ { 196 s.cursorPos(0) 197 s.eraseLine() 198 s.moveUp(1) 199 } 200 201 /* Clean the top line. */ 202 s.cursorPos(0) 203 s.eraseLine() 204 205 /* Write the prompt and the current buffer content */ 206 if _, err := fmt.Print(string(prompt)); err != nil { 207 return err 208 } 209 if _, err := fmt.Print(string(buf)); err != nil { 210 return err 211 } 212 213 /* If we are at the very end of the screen with our prompt, we need to 214 * emit a newline and move the prompt to the first column. */ 215 cursorColumns := countMultiLineGlyphs(buf[:pos], s.columns, promptColumns) 216 if cursorColumns == totalColumns && totalColumns%s.columns == 0 { 217 s.emitNewLine() 218 s.cursorPos(0) 219 totalRows++ 220 if totalRows > s.maxRows { 221 s.maxRows = totalRows 222 } 223 } 224 225 /* Move cursor to right position. */ 226 cursorRows = (cursorColumns + s.columns) / s.columns 227 if s.cursorRows > 0 && totalRows-cursorRows > 0 { 228 s.moveUp(totalRows - cursorRows) 229 } 230 /* Set column. */ 231 s.cursorPos(cursorColumns % s.columns) 232 233 s.cursorRows = cursorRows 234 return nil 235} 236 237func (s *State) resetMultiLine(prompt []rune, buf []rune, pos int) { 238 columns := countMultiLineGlyphs(prompt, s.columns, 0) 239 columns = countMultiLineGlyphs(buf[:pos], s.columns, columns) 240 columns += 2 // ^C 241 cursorRows := (columns + s.columns) / s.columns 242 if s.maxRows-cursorRows > 0 { 243 for i := 0; i < s.maxRows-cursorRows; i++ { 244 fmt.Println() // always moves the cursor down or scrolls the window up as needed 245 } 246 } 247 s.maxRows = 1 248 s.cursorRows = 0 249} 250 251func longestCommonPrefix(strs []string) string { 252 if len(strs) == 0 { 253 return "" 254 } 255 longest := strs[0] 256 257 for _, str := range strs[1:] { 258 for !strings.HasPrefix(str, longest) { 259 longest = longest[:len(longest)-1] 260 } 261 } 262 // Remove trailing partial runes 263 longest = strings.TrimRight(longest, "\uFFFD") 264 return longest 265} 266 267func (s *State) circularTabs(items []string) func(tabDirection) (string, error) { 268 item := -1 269 return func(direction tabDirection) (string, error) { 270 if direction == tabForward { 271 if item < len(items)-1 { 272 item++ 273 } else { 274 item = 0 275 } 276 } else if direction == tabReverse { 277 if item > 0 { 278 item-- 279 } else { 280 item = len(items) - 1 281 } 282 } 283 return items[item], nil 284 } 285} 286 287func calculateColumns(screenWidth int, items []string) (numColumns, numRows, maxWidth int) { 288 for _, item := range items { 289 if len(item) >= screenWidth { 290 return 1, len(items), screenWidth - 1 291 } 292 if len(item) >= maxWidth { 293 maxWidth = len(item) + 1 294 } 295 } 296 297 numColumns = screenWidth / maxWidth 298 numRows = len(items) / numColumns 299 if len(items)%numColumns > 0 { 300 numRows++ 301 } 302 303 if len(items) <= numColumns { 304 maxWidth = 0 305 } 306 307 return 308} 309 310func (s *State) printedTabs(items []string) func(tabDirection) (string, error) { 311 numTabs := 1 312 prefix := longestCommonPrefix(items) 313 return func(direction tabDirection) (string, error) { 314 if len(items) == 1 { 315 return items[0], nil 316 } 317 318 if numTabs == 2 { 319 if len(items) > 100 { 320 fmt.Printf("\nDisplay all %d possibilities? (y or n) ", len(items)) 321 prompt: 322 for { 323 next, err := s.readNext() 324 if err != nil { 325 return prefix, err 326 } 327 328 if key, ok := next.(rune); ok { 329 switch key { 330 case 'n', 'N': 331 return prefix, nil 332 case 'y', 'Y': 333 break prompt 334 case ctrlC, ctrlD, cr, lf: 335 s.restartPrompt() 336 } 337 } 338 } 339 } 340 fmt.Println("") 341 342 numColumns, numRows, maxWidth := calculateColumns(s.columns, items) 343 344 for i := 0; i < numRows; i++ { 345 for j := 0; j < numColumns*numRows; j += numRows { 346 if i+j < len(items) { 347 if maxWidth > 0 { 348 fmt.Printf("%-*.[1]*s", maxWidth, items[i+j]) 349 } else { 350 fmt.Printf("%v ", items[i+j]) 351 } 352 } 353 } 354 fmt.Println("") 355 } 356 } else { 357 numTabs++ 358 } 359 return prefix, nil 360 } 361} 362 363func (s *State) tabComplete(p []rune, line []rune, pos int) ([]rune, int, interface{}, error) { 364 if s.completer == nil { 365 return line, pos, rune(esc), nil 366 } 367 head, list, tail := s.completer(string(line), pos) 368 if len(list) <= 0 { 369 return line, pos, rune(esc), nil 370 } 371 hl := utf8.RuneCountInString(head) 372 if len(list) == 1 { 373 err := s.refresh(p, []rune(head+list[0]+tail), hl+utf8.RuneCountInString(list[0])) 374 return []rune(head + list[0] + tail), hl + utf8.RuneCountInString(list[0]), rune(esc), err 375 } 376 377 direction := tabForward 378 tabPrinter := s.circularTabs(list) 379 if s.tabStyle == TabPrints { 380 tabPrinter = s.printedTabs(list) 381 } 382 383 for { 384 pick, err := tabPrinter(direction) 385 if err != nil { 386 return line, pos, rune(esc), err 387 } 388 err = s.refresh(p, []rune(head+pick+tail), hl+utf8.RuneCountInString(pick)) 389 if err != nil { 390 return line, pos, rune(esc), err 391 } 392 393 next, err := s.readNext() 394 if err != nil { 395 return line, pos, rune(esc), err 396 } 397 if key, ok := next.(rune); ok { 398 if key == tab { 399 direction = tabForward 400 continue 401 } 402 if key == esc { 403 return line, pos, rune(esc), nil 404 } 405 } 406 if a, ok := next.(action); ok && a == shiftTab { 407 direction = tabReverse 408 continue 409 } 410 return []rune(head + pick + tail), hl + utf8.RuneCountInString(pick), next, nil 411 } 412} 413 414// reverse intelligent search, implements a bash-like history search. 415func (s *State) reverseISearch(origLine []rune, origPos int) ([]rune, int, interface{}, error) { 416 p := "(reverse-i-search)`': " 417 err := s.refresh([]rune(p), origLine, origPos) 418 if err != nil { 419 return origLine, origPos, rune(esc), err 420 } 421 422 line := []rune{} 423 pos := 0 424 foundLine := string(origLine) 425 foundPos := origPos 426 427 getLine := func() ([]rune, []rune, int) { 428 search := string(line) 429 prompt := "(reverse-i-search)`%s': " 430 return []rune(fmt.Sprintf(prompt, search)), []rune(foundLine), foundPos 431 } 432 433 history, positions := s.getHistoryByPattern(string(line)) 434 historyPos := len(history) - 1 435 436 for { 437 next, err := s.readNext() 438 if err != nil { 439 return []rune(foundLine), foundPos, rune(esc), err 440 } 441 442 switch v := next.(type) { 443 case rune: 444 switch v { 445 case ctrlR: // Search backwards 446 if historyPos > 0 && historyPos < len(history) { 447 historyPos-- 448 foundLine = history[historyPos] 449 foundPos = positions[historyPos] 450 } else { 451 fmt.Print(beep) 452 } 453 case ctrlS: // Search forward 454 if historyPos < len(history)-1 && historyPos >= 0 { 455 historyPos++ 456 foundLine = history[historyPos] 457 foundPos = positions[historyPos] 458 } else { 459 fmt.Print(beep) 460 } 461 case ctrlH, bs: // Backspace 462 if pos <= 0 { 463 fmt.Print(beep) 464 } else { 465 n := len(getSuffixGlyphs(line[:pos], 1)) 466 line = append(line[:pos-n], line[pos:]...) 467 pos -= n 468 469 // For each char deleted, display the last matching line of history 470 history, positions := s.getHistoryByPattern(string(line)) 471 historyPos = len(history) - 1 472 if len(history) > 0 { 473 foundLine = history[historyPos] 474 foundPos = positions[historyPos] 475 } else { 476 foundLine = "" 477 foundPos = 0 478 } 479 } 480 case ctrlG: // Cancel 481 return origLine, origPos, rune(esc), err 482 483 case tab, cr, lf, ctrlA, ctrlB, ctrlD, ctrlE, ctrlF, ctrlK, 484 ctrlL, ctrlN, ctrlO, ctrlP, ctrlQ, ctrlT, ctrlU, ctrlV, ctrlW, ctrlX, ctrlY, ctrlZ: 485 fallthrough 486 case 0, ctrlC, esc, 28, 29, 30, 31: 487 return []rune(foundLine), foundPos, next, err 488 default: 489 line = append(line[:pos], append([]rune{v}, line[pos:]...)...) 490 pos++ 491 492 // For each keystroke typed, display the last matching line of history 493 history, positions = s.getHistoryByPattern(string(line)) 494 historyPos = len(history) - 1 495 if len(history) > 0 { 496 foundLine = history[historyPos] 497 foundPos = positions[historyPos] 498 } else { 499 foundLine = "" 500 foundPos = 0 501 } 502 } 503 case action: 504 return []rune(foundLine), foundPos, next, err 505 } 506 err = s.refresh(getLine()) 507 if err != nil { 508 return []rune(foundLine), foundPos, rune(esc), err 509 } 510 } 511} 512 513// addToKillRing adds some text to the kill ring. If mode is 0 it adds it to a 514// new node in the end of the kill ring, and move the current pointer to the new 515// node. If mode is 1 or 2 it appends or prepends the text to the current entry 516// of the killRing. 517func (s *State) addToKillRing(text []rune, mode int) { 518 // Don't use the same underlying array as text 519 killLine := make([]rune, len(text)) 520 copy(killLine, text) 521 522 // Point killRing to a newNode, procedure depends on the killring state and 523 // append mode. 524 if mode == 0 { // Add new node to killRing 525 if s.killRing == nil { // if killring is empty, create a new one 526 s.killRing = ring.New(1) 527 } else if s.killRing.Len() >= KillRingMax { // if killring is "full" 528 s.killRing = s.killRing.Next() 529 } else { // Normal case 530 s.killRing.Link(ring.New(1)) 531 s.killRing = s.killRing.Next() 532 } 533 } else { 534 if s.killRing == nil { // if killring is empty, create a new one 535 s.killRing = ring.New(1) 536 s.killRing.Value = []rune{} 537 } 538 if mode == 1 { // Append to last entry 539 killLine = append(s.killRing.Value.([]rune), killLine...) 540 } else if mode == 2 { // Prepend to last entry 541 killLine = append(killLine, s.killRing.Value.([]rune)...) 542 } 543 } 544 545 // Save text in the current killring node 546 s.killRing.Value = killLine 547} 548 549func (s *State) yank(p []rune, text []rune, pos int) ([]rune, int, interface{}, error) { 550 if s.killRing == nil { 551 return text, pos, rune(esc), nil 552 } 553 554 lineStart := text[:pos] 555 lineEnd := text[pos:] 556 var line []rune 557 558 for { 559 value := s.killRing.Value.([]rune) 560 line = make([]rune, 0) 561 line = append(line, lineStart...) 562 line = append(line, value...) 563 line = append(line, lineEnd...) 564 565 pos = len(lineStart) + len(value) 566 err := s.refresh(p, line, pos) 567 if err != nil { 568 return line, pos, 0, err 569 } 570 571 next, err := s.readNext() 572 if err != nil { 573 return line, pos, next, err 574 } 575 576 switch v := next.(type) { 577 case rune: 578 return line, pos, next, nil 579 case action: 580 switch v { 581 case altY: 582 s.killRing = s.killRing.Prev() 583 default: 584 return line, pos, next, nil 585 } 586 } 587 } 588} 589 590// Prompt displays p and returns a line of user input, not including a trailing 591// newline character. An io.EOF error is returned if the user signals end-of-file 592// by pressing Ctrl-D. Prompt allows line editing if the terminal supports it. 593func (s *State) Prompt(prompt string) (string, error) { 594 return s.PromptWithSuggestion(prompt, "", 0) 595} 596 597// PromptWithSuggestion displays prompt and an editable text with cursor at 598// given position. The cursor will be set to the end of the line if given position 599// is negative or greater than length of text (in runes). Returns a line of user input, not 600// including a trailing newline character. An io.EOF error is returned if the user 601// signals end-of-file by pressing Ctrl-D. 602func (s *State) PromptWithSuggestion(prompt string, text string, pos int) (string, error) { 603 for _, r := range prompt { 604 if unicode.Is(unicode.C, r) { 605 return "", ErrInvalidPrompt 606 } 607 } 608 if s.inputRedirected || !s.terminalSupported { 609 return s.promptUnsupported(prompt) 610 } 611 p := []rune(prompt) 612 const minWorkingSpace = 10 613 if s.columns < countGlyphs(p)+minWorkingSpace { 614 return s.tooNarrow(prompt) 615 } 616 if s.outputRedirected { 617 return "", ErrNotTerminalOutput 618 } 619 620 s.historyMutex.RLock() 621 defer s.historyMutex.RUnlock() 622 623 fmt.Print(prompt) 624 var line = []rune(text) 625 historyEnd := "" 626 var historyPrefix []string 627 historyPos := 0 628 historyStale := true 629 historyAction := false // used to mark history related actions 630 killAction := 0 // used to mark kill related actions 631 632 defer s.stopPrompt() 633 634 if pos < 0 || len(line) < pos { 635 pos = len(line) 636 } 637 if len(line) > 0 { 638 err := s.refresh(p, line, pos) 639 if err != nil { 640 return "", err 641 } 642 } 643 644restart: 645 s.startPrompt() 646 s.getColumns() 647 648mainLoop: 649 for { 650 next, err := s.readNext() 651 haveNext: 652 if err != nil { 653 if s.shouldRestart != nil && s.shouldRestart(err) { 654 goto restart 655 } 656 return "", err 657 } 658 659 historyAction = false 660 switch v := next.(type) { 661 case rune: 662 switch v { 663 case cr, lf: 664 if s.needRefresh { 665 err := s.refresh(p, line, pos) 666 if err != nil { 667 return "", err 668 } 669 } 670 if s.multiLineMode { 671 s.resetMultiLine(p, line, pos) 672 } 673 fmt.Println() 674 break mainLoop 675 case ctrlA: // Start of line 676 pos = 0 677 s.needRefresh = true 678 case ctrlE: // End of line 679 pos = len(line) 680 s.needRefresh = true 681 case ctrlB: // left 682 if pos > 0 { 683 pos -= len(getSuffixGlyphs(line[:pos], 1)) 684 s.needRefresh = true 685 } else { 686 fmt.Print(beep) 687 } 688 case ctrlF: // right 689 if pos < len(line) { 690 pos += len(getPrefixGlyphs(line[pos:], 1)) 691 s.needRefresh = true 692 } else { 693 fmt.Print(beep) 694 } 695 case ctrlD: // del 696 if pos == 0 && len(line) == 0 { 697 // exit 698 return "", io.EOF 699 } 700 701 // ctrlD is a potential EOF, so the rune reader shuts down. 702 // Therefore, if it isn't actually an EOF, we must re-startPrompt. 703 s.restartPrompt() 704 705 if pos >= len(line) { 706 fmt.Print(beep) 707 } else { 708 n := len(getPrefixGlyphs(line[pos:], 1)) 709 line = append(line[:pos], line[pos+n:]...) 710 s.needRefresh = true 711 } 712 case ctrlK: // delete remainder of line 713 if pos >= len(line) { 714 fmt.Print(beep) 715 } else { 716 if killAction > 0 { 717 s.addToKillRing(line[pos:], 1) // Add in apend mode 718 } else { 719 s.addToKillRing(line[pos:], 0) // Add in normal mode 720 } 721 722 killAction = 2 // Mark that there was a kill action 723 line = line[:pos] 724 s.needRefresh = true 725 } 726 case ctrlP: // up 727 historyAction = true 728 if historyStale { 729 historyPrefix = s.getHistoryByPrefix(string(line)) 730 historyPos = len(historyPrefix) 731 historyStale = false 732 } 733 if historyPos > 0 { 734 if historyPos == len(historyPrefix) { 735 historyEnd = string(line) 736 } 737 historyPos-- 738 line = []rune(historyPrefix[historyPos]) 739 pos = len(line) 740 s.needRefresh = true 741 } else { 742 fmt.Print(beep) 743 } 744 case ctrlN: // down 745 historyAction = true 746 if historyStale { 747 historyPrefix = s.getHistoryByPrefix(string(line)) 748 historyPos = len(historyPrefix) 749 historyStale = false 750 } 751 if historyPos < len(historyPrefix) { 752 historyPos++ 753 if historyPos == len(historyPrefix) { 754 line = []rune(historyEnd) 755 } else { 756 line = []rune(historyPrefix[historyPos]) 757 } 758 pos = len(line) 759 s.needRefresh = true 760 } else { 761 fmt.Print(beep) 762 } 763 case ctrlT: // transpose prev glyph with glyph under cursor 764 if len(line) < 2 || pos < 1 { 765 fmt.Print(beep) 766 } else { 767 if pos == len(line) { 768 pos -= len(getSuffixGlyphs(line, 1)) 769 } 770 prev := getSuffixGlyphs(line[:pos], 1) 771 next := getPrefixGlyphs(line[pos:], 1) 772 scratch := make([]rune, len(prev)) 773 copy(scratch, prev) 774 copy(line[pos-len(prev):], next) 775 copy(line[pos-len(prev)+len(next):], scratch) 776 pos += len(next) 777 s.needRefresh = true 778 } 779 case ctrlL: // clear screen 780 s.eraseScreen() 781 s.needRefresh = true 782 case ctrlC: // reset 783 fmt.Println("^C") 784 if s.multiLineMode { 785 s.resetMultiLine(p, line, pos) 786 } 787 if s.ctrlCAborts { 788 return "", ErrPromptAborted 789 } 790 line = line[:0] 791 pos = 0 792 fmt.Print(prompt) 793 s.restartPrompt() 794 case ctrlH, bs: // Backspace 795 if pos <= 0 { 796 fmt.Print(beep) 797 } else { 798 n := len(getSuffixGlyphs(line[:pos], 1)) 799 line = append(line[:pos-n], line[pos:]...) 800 pos -= n 801 s.needRefresh = true 802 } 803 case ctrlU: // Erase line before cursor 804 if killAction > 0 { 805 s.addToKillRing(line[:pos], 2) // Add in prepend mode 806 } else { 807 s.addToKillRing(line[:pos], 0) // Add in normal mode 808 } 809 810 killAction = 2 // Mark that there was some killing 811 line = line[pos:] 812 pos = 0 813 s.needRefresh = true 814 case ctrlW: // Erase word 815 if pos == 0 { 816 fmt.Print(beep) 817 break 818 } 819 // Remove whitespace to the left 820 var buf []rune // Store the deleted chars in a buffer 821 for { 822 if pos == 0 || !unicode.IsSpace(line[pos-1]) { 823 break 824 } 825 buf = append(buf, line[pos-1]) 826 line = append(line[:pos-1], line[pos:]...) 827 pos-- 828 } 829 // Remove non-whitespace to the left 830 for { 831 if pos == 0 || unicode.IsSpace(line[pos-1]) { 832 break 833 } 834 buf = append(buf, line[pos-1]) 835 line = append(line[:pos-1], line[pos:]...) 836 pos-- 837 } 838 // Invert the buffer and save the result on the killRing 839 var newBuf []rune 840 for i := len(buf) - 1; i >= 0; i-- { 841 newBuf = append(newBuf, buf[i]) 842 } 843 if killAction > 0 { 844 s.addToKillRing(newBuf, 2) // Add in prepend mode 845 } else { 846 s.addToKillRing(newBuf, 0) // Add in normal mode 847 } 848 killAction = 2 // Mark that there was some killing 849 850 s.needRefresh = true 851 case ctrlY: // Paste from Yank buffer 852 line, pos, next, err = s.yank(p, line, pos) 853 goto haveNext 854 case ctrlR: // Reverse Search 855 line, pos, next, err = s.reverseISearch(line, pos) 856 s.needRefresh = true 857 goto haveNext 858 case tab: // Tab completion 859 line, pos, next, err = s.tabComplete(p, line, pos) 860 goto haveNext 861 // Catch keys that do nothing, but you don't want them to beep 862 case esc: 863 // DO NOTHING 864 // Unused keys 865 case ctrlG, ctrlO, ctrlQ, ctrlS, ctrlV, ctrlX, ctrlZ: 866 fallthrough 867 // Catch unhandled control codes (anything <= 31) 868 case 0, 28, 29, 30, 31: 869 fmt.Print(beep) 870 default: 871 if pos == len(line) && !s.multiLineMode && 872 len(p)+len(line) < s.columns*4 && // Avoid countGlyphs on large lines 873 countGlyphs(p)+countGlyphs(line) < s.columns-1 { 874 line = append(line, v) 875 fmt.Printf("%c", v) 876 pos++ 877 } else { 878 line = append(line[:pos], append([]rune{v}, line[pos:]...)...) 879 pos++ 880 s.needRefresh = true 881 } 882 } 883 case action: 884 switch v { 885 case del: 886 if pos >= len(line) { 887 fmt.Print(beep) 888 } else { 889 n := len(getPrefixGlyphs(line[pos:], 1)) 890 line = append(line[:pos], line[pos+n:]...) 891 } 892 case left: 893 if pos > 0 { 894 pos -= len(getSuffixGlyphs(line[:pos], 1)) 895 } else { 896 fmt.Print(beep) 897 } 898 case wordLeft, altB: 899 if pos > 0 { 900 var spaceHere, spaceLeft, leftKnown bool 901 for { 902 pos-- 903 if pos == 0 { 904 break 905 } 906 if leftKnown { 907 spaceHere = spaceLeft 908 } else { 909 spaceHere = unicode.IsSpace(line[pos]) 910 } 911 spaceLeft, leftKnown = unicode.IsSpace(line[pos-1]), true 912 if !spaceHere && spaceLeft { 913 break 914 } 915 } 916 } else { 917 fmt.Print(beep) 918 } 919 case right: 920 if pos < len(line) { 921 pos += len(getPrefixGlyphs(line[pos:], 1)) 922 } else { 923 fmt.Print(beep) 924 } 925 case wordRight, altF: 926 if pos < len(line) { 927 var spaceHere, spaceLeft, hereKnown bool 928 for { 929 pos++ 930 if pos == len(line) { 931 break 932 } 933 if hereKnown { 934 spaceLeft = spaceHere 935 } else { 936 spaceLeft = unicode.IsSpace(line[pos-1]) 937 } 938 spaceHere, hereKnown = unicode.IsSpace(line[pos]), true 939 if spaceHere && !spaceLeft { 940 break 941 } 942 } 943 } else { 944 fmt.Print(beep) 945 } 946 case up: 947 historyAction = true 948 if historyStale { 949 historyPrefix = s.getHistoryByPrefix(string(line)) 950 historyPos = len(historyPrefix) 951 historyStale = false 952 } 953 if historyPos > 0 { 954 if historyPos == len(historyPrefix) { 955 historyEnd = string(line) 956 } 957 historyPos-- 958 line = []rune(historyPrefix[historyPos]) 959 pos = len(line) 960 } else { 961 fmt.Print(beep) 962 } 963 case down: 964 historyAction = true 965 if historyStale { 966 historyPrefix = s.getHistoryByPrefix(string(line)) 967 historyPos = len(historyPrefix) 968 historyStale = false 969 } 970 if historyPos < len(historyPrefix) { 971 historyPos++ 972 if historyPos == len(historyPrefix) { 973 line = []rune(historyEnd) 974 } else { 975 line = []rune(historyPrefix[historyPos]) 976 } 977 pos = len(line) 978 } else { 979 fmt.Print(beep) 980 } 981 case home: // Start of line 982 pos = 0 983 case end: // End of line 984 pos = len(line) 985 case altD: // Delete next word 986 if pos == len(line) { 987 fmt.Print(beep) 988 break 989 } 990 // Remove whitespace to the right 991 var buf []rune // Store the deleted chars in a buffer 992 for { 993 if pos == len(line) || !unicode.IsSpace(line[pos]) { 994 break 995 } 996 buf = append(buf, line[pos]) 997 line = append(line[:pos], line[pos+1:]...) 998 } 999 // Remove non-whitespace to the right 1000 for { 1001 if pos == len(line) || unicode.IsSpace(line[pos]) { 1002 break 1003 } 1004 buf = append(buf, line[pos]) 1005 line = append(line[:pos], line[pos+1:]...) 1006 } 1007 // Save the result on the killRing 1008 if killAction > 0 { 1009 s.addToKillRing(buf, 2) // Add in prepend mode 1010 } else { 1011 s.addToKillRing(buf, 0) // Add in normal mode 1012 } 1013 killAction = 2 // Mark that there was some killing 1014 case winch: // Window change 1015 if s.multiLineMode { 1016 if s.maxRows-s.cursorRows > 0 { 1017 s.moveDown(s.maxRows - s.cursorRows) 1018 } 1019 for i := 0; i < s.maxRows-1; i++ { 1020 s.cursorPos(0) 1021 s.eraseLine() 1022 s.moveUp(1) 1023 } 1024 s.maxRows = 1 1025 s.cursorRows = 1 1026 } 1027 } 1028 s.needRefresh = true 1029 } 1030 if s.needRefresh && !s.inputWaiting() { 1031 err := s.refresh(p, line, pos) 1032 if err != nil { 1033 return "", err 1034 } 1035 } 1036 if !historyAction { 1037 historyStale = true 1038 } 1039 if killAction > 0 { 1040 killAction-- 1041 } 1042 } 1043 return string(line), nil 1044} 1045 1046// PasswordPrompt displays p, and then waits for user input. The input typed by 1047// the user is not displayed in the terminal. 1048func (s *State) PasswordPrompt(prompt string) (string, error) { 1049 for _, r := range prompt { 1050 if unicode.Is(unicode.C, r) { 1051 return "", ErrInvalidPrompt 1052 } 1053 } 1054 if !s.terminalSupported || s.columns == 0 { 1055 return "", errors.New("liner: function not supported in this terminal") 1056 } 1057 if s.inputRedirected { 1058 return s.promptUnsupported(prompt) 1059 } 1060 if s.outputRedirected { 1061 return "", ErrNotTerminalOutput 1062 } 1063 1064 p := []rune(prompt) 1065 const minWorkingSpace = 1 1066 if s.columns < countGlyphs(p)+minWorkingSpace { 1067 return s.tooNarrow(prompt) 1068 } 1069 1070 defer s.stopPrompt() 1071 1072restart: 1073 s.startPrompt() 1074 s.getColumns() 1075 1076 fmt.Print(prompt) 1077 var line []rune 1078 pos := 0 1079 1080mainLoop: 1081 for { 1082 next, err := s.readNext() 1083 if err != nil { 1084 if s.shouldRestart != nil && s.shouldRestart(err) { 1085 goto restart 1086 } 1087 return "", err 1088 } 1089 1090 switch v := next.(type) { 1091 case rune: 1092 switch v { 1093 case cr, lf: 1094 if s.needRefresh { 1095 err := s.refresh(p, line, pos) 1096 if err != nil { 1097 return "", err 1098 } 1099 } 1100 if s.multiLineMode { 1101 s.resetMultiLine(p, line, pos) 1102 } 1103 fmt.Println() 1104 break mainLoop 1105 case ctrlD: // del 1106 if pos == 0 && len(line) == 0 { 1107 // exit 1108 return "", io.EOF 1109 } 1110 1111 // ctrlD is a potential EOF, so the rune reader shuts down. 1112 // Therefore, if it isn't actually an EOF, we must re-startPrompt. 1113 s.restartPrompt() 1114 case ctrlL: // clear screen 1115 s.eraseScreen() 1116 err := s.refresh(p, []rune{}, 0) 1117 if err != nil { 1118 return "", err 1119 } 1120 case ctrlH, bs: // Backspace 1121 if pos <= 0 { 1122 fmt.Print(beep) 1123 } else { 1124 n := len(getSuffixGlyphs(line[:pos], 1)) 1125 line = append(line[:pos-n], line[pos:]...) 1126 pos -= n 1127 } 1128 case ctrlC: 1129 fmt.Println("^C") 1130 if s.multiLineMode { 1131 s.resetMultiLine(p, line, pos) 1132 } 1133 if s.ctrlCAborts { 1134 return "", ErrPromptAborted 1135 } 1136 line = line[:0] 1137 pos = 0 1138 fmt.Print(prompt) 1139 s.restartPrompt() 1140 // Unused keys 1141 case esc, tab, ctrlA, ctrlB, ctrlE, ctrlF, ctrlG, ctrlK, ctrlN, ctrlO, ctrlP, ctrlQ, ctrlR, ctrlS, 1142 ctrlT, ctrlU, ctrlV, ctrlW, ctrlX, ctrlY, ctrlZ: 1143 fallthrough 1144 // Catch unhandled control codes (anything <= 31) 1145 case 0, 28, 29, 30, 31: 1146 fmt.Print(beep) 1147 default: 1148 line = append(line[:pos], append([]rune{v}, line[pos:]...)...) 1149 pos++ 1150 } 1151 } 1152 } 1153 return string(line), nil 1154} 1155 1156func (s *State) tooNarrow(prompt string) (string, error) { 1157 // Docker and OpenWRT and etc sometimes return 0 column width 1158 // Reset mode temporarily. Restore baked mode in case the terminal 1159 // is wide enough for the next Prompt attempt. 1160 m, merr := TerminalMode() 1161 s.origMode.ApplyMode() 1162 if merr == nil { 1163 defer m.ApplyMode() 1164 } 1165 if s.r == nil { 1166 // Windows does not always set s.r 1167 s.r = bufio.NewReader(os.Stdin) 1168 defer func() { s.r = nil }() 1169 } 1170 return s.promptUnsupported(prompt) 1171} 1172