| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129 |
- // +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
- 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)
- 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 {
- 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 {
- 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
- }
- 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. 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(text) < pos {
- pos = len(text)
- }
- 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 {
- fmt.Print(beep)
- }
- case ctrlF: // right
- if pos < len(line) {
- pos += len(getPrefixGlyphs(line[pos:], 1))
- s.needRefresh = true
- } 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.needRefresh = true
- }
- 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.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 {
- fmt.Print(beep)
- }
- 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 {
- 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.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 {
- fmt.Print(beep)
- } 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
- 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.needRefresh = true
- 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:
- fmt.Print(beep)
- 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) {
- 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 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 {
- fmt.Print(beep)
- }
- 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 {
- 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.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)
- const minWorkingSpace = 1
- if s.columns < countGlyphs(p)+minWorkingSpace {
- return s.tooNarrow(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:
- 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 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 {
- 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
- }
- 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)
- }
|