12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169 |
- // +build windows linux darwin openbsd freebsd netbsd
- package liner
- import (
- "bufio"
- "container/ring"
- "errors"
- "fmt"
- "io"
- "os"
- "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
- altBs // Alt+Backspace
- altD
- 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.columns == 0 {
- return ErrInternal
- }
- s.needRefresh = false
- if s.multiLineMode {
- return s.refreshMultiLine(prompt, buf, pos)
- }
- 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)
- // on some OS / terminals extra column is needed to place the cursor char
- if cursorColumn {
- bLen++
- }
- 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)
- // on some OS / terminals extra column is needed to place the cursor char
- // if cursorColumn {
- // totalColumns++
- // }
- // it looks like Multiline mode always assume that a cursor need an extra column,
- // and always emit a newline if we are at the screen end, so no worarounds needed there
- 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 {
- err := 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), err
- }
- 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
- }
- err = s.refresh(p, []rune(head+pick+tail), hl+utf8.RuneCountInString(pick))
- if err != nil {
- return line, pos, rune(esc), err
- }
- 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
- }
- }
- // 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)`': "
- err := s.refresh([]rune(p), origLine, origPos)
- if err != nil {
- return origLine, origPos, rune(esc), err
- }
- 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 {
- s.doBeep()
- }
- case ctrlS: // Search forward
- if historyPos < len(history)-1 && historyPos >= 0 {
- historyPos++
- foundLine = history[historyPos]
- foundPos = positions[historyPos]
- } else {
- s.doBeep()
- }
- case ctrlH, bs: // Backspace
- if pos <= 0 {
- s.doBeep()
- } 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
- }
- err = s.refresh(getLine())
- if err != nil {
- return []rune(foundLine), foundPos, rune(esc), err
- }
- }
- }
- // 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)
- err := s.refresh(p, line, pos)
- if err != nil {
- return line, pos, 0, err
- }
- 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
- }
- }
- }
- }
- // 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 (in runes). 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) {
- for _, r := range prompt {
- if unicode.Is(unicode.C, r) {
- return "", ErrInvalidPrompt
- }
- }
- if s.inputRedirected || !s.terminalSupported {
- return s.promptUnsupported(prompt)
- }
- p := []rune(prompt)
- const minWorkingSpace = 10
- if s.columns < countGlyphs(p)+minWorkingSpace {
- return s.tooNarrow(prompt)
- }
- if s.outputRedirected {
- return "", ErrNotTerminalOutput
- }
- s.historyMutex.RLock()
- defer s.historyMutex.RUnlock()
- fmt.Print(prompt)
- var line = []rune(text)
- historyEnd := ""
- var historyPrefix []string
- historyPos := 0
- historyStale := true
- historyAction := false // used to mark history related actions
- killAction := 0 // used to mark kill related actions
- defer s.stopPrompt()
- if pos < 0 || len(line) < pos {
- pos = len(line)
- }
- if len(line) > 0 {
- err := s.refresh(p, line, pos)
- if err != nil {
- return "", err
- }
- }
- 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.needRefresh {
- err := s.refresh(p, line, pos)
- if err != nil {
- return "", err
- }
- }
- if s.multiLineMode {
- s.resetMultiLine(p, line, pos)
- }
- fmt.Println()
- break mainLoop
- case ctrlA: // Start of line
- pos = 0
- s.needRefresh = true
- case ctrlE: // End of line
- pos = len(line)
- s.needRefresh = true
- case ctrlB: // left
- if pos > 0 {
- pos -= len(getSuffixGlyphs(line[:pos], 1))
- s.needRefresh = true
- } else {
- s.doBeep()
- }
- case ctrlF: // right
- if pos < len(line) {
- pos += len(getPrefixGlyphs(line[pos:], 1))
- s.needRefresh = true
- } else {
- s.doBeep()
- }
- 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) {
- s.doBeep()
- } else {
- n := len(getPrefixGlyphs(line[pos:], 1))
- line = append(line[:pos], line[pos+n:]...)
- s.needRefresh = true
- }
- case ctrlK: // delete remainder of line
- if pos >= len(line) {
- s.doBeep()
- } 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.needRefresh = true
- }
- case ctrlP: // up
- historyAction = true
- if historyStale {
- historyPrefix = s.getHistoryByPrefix(string(line))
- historyPos = len(historyPrefix)
- historyStale = false
- }
- if historyPos > 0 {
- if historyPos == len(historyPrefix) {
- historyEnd = string(line)
- }
- historyPos--
- line = []rune(historyPrefix[historyPos])
- pos = len(line)
- s.needRefresh = true
- } else {
- s.doBeep()
- }
- case ctrlN: // down
- historyAction = true
- if historyStale {
- historyPrefix = s.getHistoryByPrefix(string(line))
- historyPos = len(historyPrefix)
- historyStale = false
- }
- if historyPos < len(historyPrefix) {
- historyPos++
- if historyPos == len(historyPrefix) {
- line = []rune(historyEnd)
- } else {
- line = []rune(historyPrefix[historyPos])
- }
- pos = len(line)
- s.needRefresh = true
- } else {
- s.doBeep()
- }
- case ctrlT: // transpose prev glyph with glyph under cursor
- if len(line) < 2 || pos < 1 {
- s.doBeep()
- } 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.needRefresh = true
- }
- case ctrlL: // clear screen
- s.eraseScreen()
- s.needRefresh = true
- 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 {
- s.doBeep()
- } else {
- n := len(getSuffixGlyphs(line[:pos], 1))
- line = append(line[:pos-n], line[pos:]...)
- pos -= n
- s.needRefresh = true
- }
- 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.needRefresh = true
- case ctrlW: // Erase word
- pos, line, killAction = s.eraseWord(pos, line, killAction)
- 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.needRefresh = true
- 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:
- s.doBeep()
- default:
- if pos == len(line) && !s.multiLineMode &&
- len(p)+len(line) < s.columns*4 && // Avoid countGlyphs on large lines
- 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.needRefresh = true
- }
- }
- case action:
- switch v {
- case del:
- if pos >= len(line) {
- s.doBeep()
- } 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 {
- s.doBeep()
- }
- 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 {
- s.doBeep()
- }
- case right:
- if pos < len(line) {
- pos += len(getPrefixGlyphs(line[pos:], 1))
- } else {
- s.doBeep()
- }
- 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 {
- s.doBeep()
- }
- case up:
- historyAction = true
- if historyStale {
- historyPrefix = s.getHistoryByPrefix(string(line))
- historyPos = len(historyPrefix)
- historyStale = false
- }
- if historyPos > 0 {
- if historyPos == len(historyPrefix) {
- historyEnd = string(line)
- }
- historyPos--
- line = []rune(historyPrefix[historyPos])
- pos = len(line)
- } else {
- s.doBeep()
- }
- case down:
- historyAction = true
- if historyStale {
- historyPrefix = s.getHistoryByPrefix(string(line))
- historyPos = len(historyPrefix)
- historyStale = false
- }
- if historyPos < len(historyPrefix) {
- historyPos++
- if historyPos == len(historyPrefix) {
- line = []rune(historyEnd)
- } else {
- line = []rune(historyPrefix[historyPos])
- }
- pos = len(line)
- } else {
- s.doBeep()
- }
- case home: // Start of line
- pos = 0
- case end: // End of line
- pos = len(line)
- case altD: // Delete next word
- if pos == len(line) {
- s.doBeep()
- break
- }
- // Remove whitespace to the right
- var buf []rune // Store the deleted chars in a buffer
- for {
- if pos == len(line) || !unicode.IsSpace(line[pos]) {
- break
- }
- buf = append(buf, line[pos])
- line = append(line[:pos], line[pos+1:]...)
- }
- // Remove non-whitespace to the right
- for {
- if pos == len(line) || unicode.IsSpace(line[pos]) {
- break
- }
- buf = append(buf, line[pos])
- line = append(line[:pos], line[pos+1:]...)
- }
- // Save the result on the killRing
- if killAction > 0 {
- s.addToKillRing(buf, 2) // Add in prepend mode
- } else {
- s.addToKillRing(buf, 0) // Add in normal mode
- }
- killAction = 2 // Mark that there was some killing
- case altBs: // Erase word
- pos, line, killAction = s.eraseWord(pos, line, killAction)
- 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.needRefresh = true
- }
- if s.needRefresh && !s.inputWaiting() {
- err := s.refresh(p, line, pos)
- if err != nil {
- return "", err
- }
- }
- if !historyAction {
- historyStale = true
- }
- 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) {
- for _, r := range prompt {
- if unicode.Is(unicode.C, r) {
- return "", ErrInvalidPrompt
- }
- }
- if !s.terminalSupported || s.columns == 0 {
- return "", errors.New("liner: function not supported in this terminal")
- }
- if s.inputRedirected {
- return s.promptUnsupported(prompt)
- }
- if s.outputRedirected {
- return "", ErrNotTerminalOutput
- }
- p := []rune(prompt)
- defer s.stopPrompt()
- restart:
- s.startPrompt()
- s.getColumns()
- fmt.Print(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:
- 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()
- err := s.refresh(p, []rune{}, 0)
- if err != nil {
- return "", err
- }
- case ctrlH, bs: // Backspace
- if pos <= 0 {
- s.doBeep()
- } else {
- n := len(getSuffixGlyphs(line[:pos], 1))
- line = append(line[:pos-n], line[pos:]...)
- pos -= n
- }
- case ctrlC:
- fmt.Println("^C")
- 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:
- s.doBeep()
- default:
- line = append(line[:pos], append([]rune{v}, line[pos:]...)...)
- pos++
- }
- }
- }
- return string(line), nil
- }
- func (s *State) tooNarrow(prompt string) (string, error) {
- // Docker and OpenWRT and etc sometimes return 0 column width
- // Reset mode temporarily. Restore baked mode in case the terminal
- // is wide enough for the next Prompt attempt.
- m, merr := TerminalMode()
- s.origMode.ApplyMode()
- if merr == nil {
- defer m.ApplyMode()
- }
- if s.r == nil {
- // Windows does not always set s.r
- s.r = bufio.NewReader(os.Stdin)
- defer func() { s.r = nil }()
- }
- return s.promptUnsupported(prompt)
- }
- func (s *State) eraseWord(pos int, line []rune, killAction int) (int, []rune, int) {
- if pos == 0 {
- s.doBeep()
- return pos, line, killAction
- }
- // 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.needRefresh = true
- return pos, line, killAction
- }
- func (s *State) doBeep() {
- if !s.noBeep {
- fmt.Print(beep)
- }
- }
|