mirror of https://github.com/ledisdb/ledisdb.git
1160 lines
26 KiB
Go
1160 lines
26 KiB
Go
// +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
|
|
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)
|
|
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 altD: // Delete next word
|
|
if pos == len(line) {
|
|
fmt.Print(beep)
|
|
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 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)
|
|
}
|