// +build windows linux darwin openbsd freebsd netbsd package liner import ( "container/ring" "errors" "fmt" "io" "strings" "unicode" "unicode/utf8" ) type action int const ( left action = iota right up down home end insert del pageUp pageDown f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 altB altF altY shiftTab wordLeft wordRight winch unknown ) const ( ctrlA = 1 ctrlB = 2 ctrlC = 3 ctrlD = 4 ctrlE = 5 ctrlF = 6 ctrlG = 7 ctrlH = 8 tab = 9 lf = 10 ctrlK = 11 ctrlL = 12 cr = 13 ctrlN = 14 ctrlO = 15 ctrlP = 16 ctrlQ = 17 ctrlR = 18 ctrlS = 19 ctrlT = 20 ctrlU = 21 ctrlV = 22 ctrlW = 23 ctrlX = 24 ctrlY = 25 ctrlZ = 26 esc = 27 bs = 127 ) const ( beep = "\a" ) type tabDirection int const ( tabForward tabDirection = iota tabReverse ) func (s *State) refresh(prompt []rune, buf []rune, pos int) error { if s.multiLineMode { return s.refreshMultiLine(prompt, buf, pos) } else { return s.refreshSingleLine(prompt, buf, pos) } } func (s *State) refreshSingleLine(prompt []rune, buf []rune, pos int) error { s.cursorPos(0) _, err := fmt.Print(string(prompt)) if err != nil { return err } pLen := countGlyphs(prompt) bLen := countGlyphs(buf) pos = countGlyphs(buf[:pos]) if pLen+bLen < s.columns { _, err = fmt.Print(string(buf)) s.eraseLine() s.cursorPos(pLen + pos) } else { // Find space available space := s.columns - pLen space-- // space for cursor start := pos - space/2 end := start + space if end > bLen { end = bLen start = end - space } if start < 0 { start = 0 end = space } pos -= start // Leave space for markers if start > 0 { start++ } if end < bLen { end-- } startRune := len(getPrefixGlyphs(buf, start)) line := getPrefixGlyphs(buf[startRune:], end-start) // Output if start > 0 { fmt.Print("{") } fmt.Print(string(line)) if end < bLen { fmt.Print("}") } // Set cursor position s.eraseLine() s.cursorPos(pLen + pos) } return err } func (s *State) refreshMultiLine(prompt []rune, buf []rune, pos int) error { promptColumns := countMultiLineGlyphs(prompt, s.columns, 0) totalColumns := countMultiLineGlyphs(buf, s.columns, promptColumns) totalRows := (totalColumns + s.columns - 1) / s.columns maxRows := s.maxRows if totalRows > s.maxRows { s.maxRows = totalRows } cursorRows := s.cursorRows if cursorRows == 0 { cursorRows = 1 } /* First step: clear all the lines used before. To do so start by * going to the last row. */ if maxRows-cursorRows > 0 { s.moveDown(maxRows - cursorRows) } /* Now for every row clear it, go up. */ for i := 0; i < maxRows-1; i++ { s.cursorPos(0) s.eraseLine() s.moveUp(1) } /* Clean the top line. */ s.cursorPos(0) s.eraseLine() /* Write the prompt and the current buffer content */ if _, err := fmt.Print(string(prompt)); err != nil { return err } if _, err := fmt.Print(string(buf)); err != nil { return err } /* If we are at the very end of the screen with our prompt, we need to * emit a newline and move the prompt to the first column. */ cursorColumns := countMultiLineGlyphs(buf[:pos], s.columns, promptColumns) if cursorColumns == totalColumns && totalColumns%s.columns == 0 { s.emitNewLine() s.cursorPos(0) totalRows++ if totalRows > s.maxRows { s.maxRows = totalRows } } /* Move cursor to right position. */ cursorRows = (cursorColumns + s.columns) / s.columns if s.cursorRows > 0 && totalRows-cursorRows > 0 { s.moveUp(totalRows - cursorRows) } /* Set column. */ s.cursorPos(cursorColumns % s.columns) s.cursorRows = cursorRows return nil } func (s *State) resetMultiLine(prompt []rune, buf []rune, pos int) { columns := countMultiLineGlyphs(prompt, s.columns, 0) columns = countMultiLineGlyphs(buf[:pos], s.columns, columns) columns += 2 // ^C cursorRows := (columns + s.columns) / s.columns if s.maxRows-cursorRows > 0 { for i := 0; i < s.maxRows-cursorRows; i++ { fmt.Println() // always moves the cursor down or scrolls the window up as needed } } s.maxRows = 1 s.cursorRows = 0 } func longestCommonPrefix(strs []string) string { if len(strs) == 0 { return "" } longest := strs[0] for _, str := range strs[1:] { for !strings.HasPrefix(str, longest) { longest = longest[:len(longest)-1] } } // Remove trailing partial runes longest = strings.TrimRight(longest, "\uFFFD") return longest } func (s *State) circularTabs(items []string) func(tabDirection) (string, error) { item := -1 return func(direction tabDirection) (string, error) { if direction == tabForward { if item < len(items)-1 { item++ } else { item = 0 } } else if direction == tabReverse { if item > 0 { item-- } else { item = len(items) - 1 } } return items[item], nil } } func calculateColumns(screenWidth int, items []string) (numColumns, numRows, maxWidth int) { for _, item := range items { if len(item) >= screenWidth { return 1, len(items), screenWidth - 1 } if len(item) >= maxWidth { maxWidth = len(item) + 1 } } numColumns = screenWidth / maxWidth numRows = len(items) / numColumns if len(items)%numColumns > 0 { numRows++ } if len(items) <= numColumns { maxWidth = 0 } return } func (s *State) printedTabs(items []string) func(tabDirection) (string, error) { numTabs := 1 prefix := longestCommonPrefix(items) return func(direction tabDirection) (string, error) { if len(items) == 1 { return items[0], nil } if numTabs == 2 { if len(items) > 100 { fmt.Printf("\nDisplay all %d possibilities? (y or n) ", len(items)) prompt: for { next, err := s.readNext() if err != nil { return prefix, err } if key, ok := next.(rune); ok { switch key { case 'n', 'N': return prefix, nil case 'y', 'Y': break prompt case ctrlC, ctrlD, cr, lf: s.restartPrompt() } } } } fmt.Println("") numColumns, numRows, maxWidth := calculateColumns(s.columns, items) for i := 0; i < numRows; i++ { for j := 0; j < numColumns*numRows; j += numRows { if i+j < len(items) { if maxWidth > 0 { fmt.Printf("%-*.[1]*s", maxWidth, items[i+j]) } else { fmt.Printf("%v ", items[i+j]) } } } fmt.Println("") } } else { numTabs++ } return prefix, nil } } func (s *State) tabComplete(p []rune, line []rune, pos int) ([]rune, int, interface{}, error) { if s.completer == nil { return line, pos, rune(esc), nil } head, list, tail := s.completer(string(line), pos) if len(list) <= 0 { return line, pos, rune(esc), nil } hl := utf8.RuneCountInString(head) if len(list) == 1 { s.refresh(p, []rune(head+list[0]+tail), hl+utf8.RuneCountInString(list[0])) return []rune(head + list[0] + tail), hl + utf8.RuneCountInString(list[0]), rune(esc), nil } direction := tabForward tabPrinter := s.circularTabs(list) if s.tabStyle == TabPrints { tabPrinter = s.printedTabs(list) } for { pick, err := tabPrinter(direction) if err != nil { return line, pos, rune(esc), err } s.refresh(p, []rune(head+pick+tail), hl+utf8.RuneCountInString(pick)) next, err := s.readNext() if err != nil { return line, pos, rune(esc), err } if key, ok := next.(rune); ok { if key == tab { direction = tabForward continue } if key == esc { return line, pos, rune(esc), nil } } if a, ok := next.(action); ok && a == shiftTab { direction = tabReverse continue } return []rune(head + pick + tail), hl + utf8.RuneCountInString(pick), next, nil } // Not reached return line, pos, rune(esc), nil } // reverse intelligent search, implements a bash-like history search. func (s *State) reverseISearch(origLine []rune, origPos int) ([]rune, int, interface{}, error) { p := "(reverse-i-search)`': " s.refresh([]rune(p), origLine, origPos) line := []rune{} pos := 0 foundLine := string(origLine) foundPos := origPos getLine := func() ([]rune, []rune, int) { search := string(line) prompt := "(reverse-i-search)`%s': " return []rune(fmt.Sprintf(prompt, search)), []rune(foundLine), foundPos } history, positions := s.getHistoryByPattern(string(line)) historyPos := len(history) - 1 for { next, err := s.readNext() if err != nil { return []rune(foundLine), foundPos, rune(esc), err } switch v := next.(type) { case rune: switch v { case ctrlR: // Search backwards if historyPos > 0 && historyPos < len(history) { historyPos-- foundLine = history[historyPos] foundPos = positions[historyPos] } else { fmt.Print(beep) } case ctrlS: // Search forward if historyPos < len(history)-1 && historyPos >= 0 { historyPos++ foundLine = history[historyPos] foundPos = positions[historyPos] } else { fmt.Print(beep) } case ctrlH, bs: // Backspace if pos <= 0 { fmt.Print(beep) } else { n := len(getSuffixGlyphs(line[:pos], 1)) line = append(line[:pos-n], line[pos:]...) pos -= n // For each char deleted, display the last matching line of history history, positions := s.getHistoryByPattern(string(line)) historyPos = len(history) - 1 if len(history) > 0 { foundLine = history[historyPos] foundPos = positions[historyPos] } else { foundLine = "" foundPos = 0 } } case ctrlG: // Cancel return origLine, origPos, rune(esc), err case tab, cr, lf, ctrlA, ctrlB, ctrlD, ctrlE, ctrlF, ctrlK, ctrlL, ctrlN, ctrlO, ctrlP, ctrlQ, ctrlT, ctrlU, ctrlV, ctrlW, ctrlX, ctrlY, ctrlZ: fallthrough case 0, ctrlC, esc, 28, 29, 30, 31: return []rune(foundLine), foundPos, next, err default: line = append(line[:pos], append([]rune{v}, line[pos:]...)...) pos++ // For each keystroke typed, display the last matching line of history history, positions = s.getHistoryByPattern(string(line)) historyPos = len(history) - 1 if len(history) > 0 { foundLine = history[historyPos] foundPos = positions[historyPos] } else { foundLine = "" foundPos = 0 } } case action: return []rune(foundLine), foundPos, next, err } s.refresh(getLine()) } } // addToKillRing adds some text to the kill ring. If mode is 0 it adds it to a // new node in the end of the kill ring, and move the current pointer to the new // node. If mode is 1 or 2 it appends or prepends the text to the current entry // of the killRing. func (s *State) addToKillRing(text []rune, mode int) { // Don't use the same underlying array as text killLine := make([]rune, len(text)) copy(killLine, text) // Point killRing to a newNode, procedure depends on the killring state and // append mode. if mode == 0 { // Add new node to killRing if s.killRing == nil { // if killring is empty, create a new one s.killRing = ring.New(1) } else if s.killRing.Len() >= KillRingMax { // if killring is "full" s.killRing = s.killRing.Next() } else { // Normal case s.killRing.Link(ring.New(1)) s.killRing = s.killRing.Next() } } else { if s.killRing == nil { // if killring is empty, create a new one s.killRing = ring.New(1) s.killRing.Value = []rune{} } if mode == 1 { // Append to last entry killLine = append(s.killRing.Value.([]rune), killLine...) } else if mode == 2 { // Prepend to last entry killLine = append(killLine, s.killRing.Value.([]rune)...) } } // Save text in the current killring node s.killRing.Value = killLine } func (s *State) yank(p []rune, text []rune, pos int) ([]rune, int, interface{}, error) { if s.killRing == nil { return text, pos, rune(esc), nil } lineStart := text[:pos] lineEnd := text[pos:] var line []rune for { value := s.killRing.Value.([]rune) line = make([]rune, 0) line = append(line, lineStart...) line = append(line, value...) line = append(line, lineEnd...) pos = len(lineStart) + len(value) s.refresh(p, line, pos) next, err := s.readNext() if err != nil { return line, pos, next, err } switch v := next.(type) { case rune: return line, pos, next, nil case action: switch v { case altY: s.killRing = s.killRing.Prev() default: return line, pos, next, nil } } } return line, pos, esc, nil } // Prompt displays p and returns a line of user input, not including a trailing // newline character. An io.EOF error is returned if the user signals end-of-file // by pressing Ctrl-D. Prompt allows line editing if the terminal supports it. func (s *State) Prompt(prompt string) (string, error) { return s.PromptWithSuggestion(prompt, "", 0) } // PromptWithSuggestion displays prompt and an editable text with cursor at // given position. The cursor will be set to the end of the line if given position // is negative or greater than length of text. Returns a line of user input, not // including a trailing newline character. An io.EOF error is returned if the user // signals end-of-file by pressing Ctrl-D. func (s *State) PromptWithSuggestion(prompt string, text string, pos int) (string, error) { if s.inputRedirected || !s.terminalSupported { return s.promptUnsupported(prompt) } if s.outputRedirected { return "", ErrNotTerminalOutput } s.historyMutex.RLock() defer s.historyMutex.RUnlock() fmt.Print(prompt) p := []rune(prompt) var line = []rune(text) historyEnd := "" prefixHistory := s.getHistoryByPrefix(string(line)) historyPos := len(prefixHistory) historyAction := false // used to mark history related actions killAction := 0 // used to mark kill related actions defer s.stopPrompt() if pos < 0 || len(text) < pos { pos = len(text) } if len(line) > 0 { s.refresh(p, line, pos) } restart: s.startPrompt() s.getColumns() mainLoop: for { next, err := s.readNext() haveNext: if err != nil { if s.shouldRestart != nil && s.shouldRestart(err) { goto restart } return "", err } historyAction = false switch v := next.(type) { case rune: switch v { case cr, lf: if s.multiLineMode { s.resetMultiLine(p, line, pos) } fmt.Println() break mainLoop case ctrlA: // Start of line pos = 0 s.refresh(p, line, pos) case ctrlE: // End of line pos = len(line) s.refresh(p, line, pos) case ctrlB: // left if pos > 0 { pos -= len(getSuffixGlyphs(line[:pos], 1)) s.refresh(p, line, pos) } else { fmt.Print(beep) } case ctrlF: // right if pos < len(line) { pos += len(getPrefixGlyphs(line[pos:], 1)) s.refresh(p, line, pos) } else { fmt.Print(beep) } case ctrlD: // del if pos == 0 && len(line) == 0 { // exit return "", io.EOF } // ctrlD is a potential EOF, so the rune reader shuts down. // Therefore, if it isn't actually an EOF, we must re-startPrompt. s.restartPrompt() if pos >= len(line) { fmt.Print(beep) } else { n := len(getPrefixGlyphs(line[pos:], 1)) line = append(line[:pos], line[pos+n:]...) s.refresh(p, line, pos) } case ctrlK: // delete remainder of line if pos >= len(line) { fmt.Print(beep) } else { if killAction > 0 { s.addToKillRing(line[pos:], 1) // Add in apend mode } else { s.addToKillRing(line[pos:], 0) // Add in normal mode } killAction = 2 // Mark that there was a kill action line = line[:pos] s.refresh(p, line, pos) } case ctrlP: // up historyAction = true if historyPos > 0 { if historyPos == len(prefixHistory) { historyEnd = string(line) } historyPos-- line = []rune(prefixHistory[historyPos]) pos = len(line) s.refresh(p, line, pos) } else { fmt.Print(beep) } case ctrlN: // down historyAction = true if historyPos < len(prefixHistory) { historyPos++ if historyPos == len(prefixHistory) { line = []rune(historyEnd) } else { line = []rune(prefixHistory[historyPos]) } pos = len(line) s.refresh(p, line, pos) } else { fmt.Print(beep) } case ctrlT: // transpose prev glyph with glyph under cursor if len(line) < 2 || pos < 1 { fmt.Print(beep) } else { if pos == len(line) { pos -= len(getSuffixGlyphs(line, 1)) } prev := getSuffixGlyphs(line[:pos], 1) next := getPrefixGlyphs(line[pos:], 1) scratch := make([]rune, len(prev)) copy(scratch, prev) copy(line[pos-len(prev):], next) copy(line[pos-len(prev)+len(next):], scratch) pos += len(next) s.refresh(p, line, pos) } case ctrlL: // clear screen s.eraseScreen() s.refresh(p, line, pos) case ctrlC: // reset fmt.Println("^C") if s.multiLineMode { s.resetMultiLine(p, line, pos) } if s.ctrlCAborts { return "", ErrPromptAborted } line = line[:0] pos = 0 fmt.Print(prompt) s.restartPrompt() case ctrlH, bs: // Backspace if pos <= 0 { fmt.Print(beep) } else { n := len(getSuffixGlyphs(line[:pos], 1)) line = append(line[:pos-n], line[pos:]...) pos -= n s.refresh(p, line, pos) } case ctrlU: // Erase line before cursor if killAction > 0 { s.addToKillRing(line[:pos], 2) // Add in prepend mode } else { s.addToKillRing(line[:pos], 0) // Add in normal mode } killAction = 2 // Mark that there was some killing line = line[pos:] pos = 0 s.refresh(p, line, pos) case ctrlW: // Erase word if pos == 0 { fmt.Print(beep) break } // Remove whitespace to the left var buf []rune // Store the deleted chars in a buffer for { if pos == 0 || !unicode.IsSpace(line[pos-1]) { break } buf = append(buf, line[pos-1]) line = append(line[:pos-1], line[pos:]...) pos-- } // Remove non-whitespace to the left for { if pos == 0 || unicode.IsSpace(line[pos-1]) { break } buf = append(buf, line[pos-1]) line = append(line[:pos-1], line[pos:]...) pos-- } // Invert the buffer and save the result on the killRing var newBuf []rune for i := len(buf) - 1; i >= 0; i-- { newBuf = append(newBuf, buf[i]) } if killAction > 0 { s.addToKillRing(newBuf, 2) // Add in prepend mode } else { s.addToKillRing(newBuf, 0) // Add in normal mode } killAction = 2 // Mark that there was some killing s.refresh(p, line, pos) case ctrlY: // Paste from Yank buffer line, pos, next, err = s.yank(p, line, pos) goto haveNext case ctrlR: // Reverse Search line, pos, next, err = s.reverseISearch(line, pos) s.refresh(p, line, pos) goto haveNext case tab: // Tab completion line, pos, next, err = s.tabComplete(p, line, pos) goto haveNext // Catch keys that do nothing, but you don't want them to beep case esc: // DO NOTHING // Unused keys case ctrlG, ctrlO, ctrlQ, ctrlS, ctrlV, ctrlX, ctrlZ: fallthrough // Catch unhandled control codes (anything <= 31) case 0, 28, 29, 30, 31: fmt.Print(beep) default: if pos == len(line) && !s.multiLineMode && countGlyphs(p)+countGlyphs(line) < s.columns-1 { line = append(line, v) fmt.Printf("%c", v) pos++ } else { line = append(line[:pos], append([]rune{v}, line[pos:]...)...) pos++ s.refresh(p, line, pos) } } case action: switch v { case del: if pos >= len(line) { fmt.Print(beep) } else { n := len(getPrefixGlyphs(line[pos:], 1)) line = append(line[:pos], line[pos+n:]...) } case left: if pos > 0 { pos -= len(getSuffixGlyphs(line[:pos], 1)) } else { fmt.Print(beep) } case wordLeft, altB: if pos > 0 { var spaceHere, spaceLeft, leftKnown bool for { pos-- if pos == 0 { break } if leftKnown { spaceHere = spaceLeft } else { spaceHere = unicode.IsSpace(line[pos]) } spaceLeft, leftKnown = unicode.IsSpace(line[pos-1]), true if !spaceHere && spaceLeft { break } } } else { fmt.Print(beep) } case right: if pos < len(line) { pos += len(getPrefixGlyphs(line[pos:], 1)) } else { fmt.Print(beep) } case wordRight, altF: if pos < len(line) { var spaceHere, spaceLeft, hereKnown bool for { pos++ if pos == len(line) { break } if hereKnown { spaceLeft = spaceHere } else { spaceLeft = unicode.IsSpace(line[pos-1]) } spaceHere, hereKnown = unicode.IsSpace(line[pos]), true if spaceHere && !spaceLeft { break } } } else { fmt.Print(beep) } case up: historyAction = true if historyPos > 0 { if historyPos == len(prefixHistory) { historyEnd = string(line) } historyPos-- line = []rune(prefixHistory[historyPos]) pos = len(line) } else { fmt.Print(beep) } case down: historyAction = true if historyPos < len(prefixHistory) { historyPos++ if historyPos == len(prefixHistory) { line = []rune(historyEnd) } else { line = []rune(prefixHistory[historyPos]) } pos = len(line) } else { fmt.Print(beep) } case home: // Start of line pos = 0 case end: // End of line pos = len(line) case winch: // Window change if s.multiLineMode { if s.maxRows-s.cursorRows > 0 { s.moveDown(s.maxRows - s.cursorRows) } for i := 0; i < s.maxRows-1; i++ { s.cursorPos(0) s.eraseLine() s.moveUp(1) } s.maxRows = 1 s.cursorRows = 1 } } s.refresh(p, line, pos) } if !historyAction { prefixHistory = s.getHistoryByPrefix(string(line)) historyPos = len(prefixHistory) } if killAction > 0 { killAction-- } } return string(line), nil } // PasswordPrompt displays p, and then waits for user input. The input typed by // the user is not displayed in the terminal. func (s *State) PasswordPrompt(prompt string) (string, error) { if !s.terminalSupported { return "", errors.New("liner: function not supported in this terminal") } if s.inputRedirected { return s.promptUnsupported(prompt) } if s.outputRedirected { return "", ErrNotTerminalOutput } defer s.stopPrompt() restart: s.startPrompt() s.getColumns() fmt.Print(prompt) p := []rune(prompt) var line []rune pos := 0 mainLoop: for { next, err := s.readNext() if err != nil { if s.shouldRestart != nil && s.shouldRestart(err) { goto restart } return "", err } switch v := next.(type) { case rune: switch v { case cr, lf: if s.multiLineMode { s.resetMultiLine(p, line, pos) } fmt.Println() break mainLoop case ctrlD: // del if pos == 0 && len(line) == 0 { // exit return "", io.EOF } // ctrlD is a potential EOF, so the rune reader shuts down. // Therefore, if it isn't actually an EOF, we must re-startPrompt. s.restartPrompt() case ctrlL: // clear screen s.eraseScreen() s.refresh(p, []rune{}, 0) case ctrlH, bs: // Backspace if pos <= 0 { fmt.Print(beep) } else { n := len(getSuffixGlyphs(line[:pos], 1)) line = append(line[:pos-n], line[pos:]...) pos -= n } case ctrlC: fmt.Println("^C") if s.multiLineMode { s.resetMultiLine(p, line, pos) } if s.ctrlCAborts { return "", ErrPromptAborted } line = line[:0] pos = 0 fmt.Print(prompt) s.restartPrompt() // Unused keys case esc, tab, ctrlA, ctrlB, ctrlE, ctrlF, ctrlG, ctrlK, ctrlN, ctrlO, ctrlP, ctrlQ, ctrlR, ctrlS, ctrlT, ctrlU, ctrlV, ctrlW, ctrlX, ctrlY, ctrlZ: fallthrough // Catch unhandled control codes (anything <= 31) case 0, 28, 29, 30, 31: fmt.Print(beep) default: line = append(line[:pos], append([]rune{v}, line[pos:]...)...) pos++ } } } return string(line), nil }