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