package readline import ( "bytes" "errors" "fmt" "os" "regexp" "syscall" ) var rxMultiline = regexp.MustCompile(`[\r\n]+`) // Readline displays the readline prompt. // It will return a string (user entered data) or an error. func (rl *Instance) Readline() (string, error) { fd := int(os.Stdin.Fd()) state, err := MakeRaw(fd) if err != nil { return "", err } defer Restore(fd, state) // In Vim mode, we always start in Input mode. The prompt needs this. rl.modeViMode = VimInsert // Prompt Init // Here we have to either print prompt // and return new line (multiline) if rl.Multiline { fmt.Println(rl.mainPrompt) } rl.stillOnRefresh = false rl.computePrompt() // initialise the prompt for first print // Line Init & Cursor rl.line = []rune{} rl.currentComp = []rune{} // No virtual completion yet rl.lineComp = []rune{} // So no virtual line either rl.modeViMode = VimInsert rl.pos = 0 rl.posY = 0 rl.tcPrefix = "" // Completion && infos init rl.resetInfoText() rl.resetTabCompletion() rl.getInfoText() // History Init // We need this set to the last command, so that we can access it quickly rl.histPos = 0 rl.viUndoHistory = []undoItem{{line: "", pos: 0}} // Multisplit if len(rl.multisplit) > 0 { r := []rune(rl.multisplit[0]) rl.editorInput(r) rl.carridgeReturn() if len(rl.multisplit) > 1 { rl.multisplit = rl.multisplit[1:] } else { rl.multisplit = []string{} } return string(rl.line), nil } // Finally, print any info or completions // if the TabCompletion engines so desires rl.renderHelpers() // Start handling keystrokes. Classified by subject for most. for { rl.viUndoSkipAppend = false b := make([]byte, 1024) var i int if !rl.skipStdinRead { var err error i, err = os.Stdin.Read(b) if err != nil { if errors.Is(err, syscall.EAGAIN) { err = syscall.SetNonblock(syscall.Stdin, false) if err == nil { continue } } return "", err } } rl.skipStdinRead = false r := []rune(string(b)) if isMultiline(r[:i]) || len(rl.multiline) > 0 { rl.multiline = append(rl.multiline, b[:i]...) if i == len(b) { continue } if !rl.allowMultiline(rl.multiline) { rl.multiline = []byte{} continue } s := string(rl.multiline) rl.multisplit = rxMultiline.Split(s, -1) r = []rune(rl.multisplit[0]) rl.modeViMode = VimInsert rl.editorInput(r) rl.carridgeReturn() rl.multiline = []byte{} if len(rl.multisplit) > 1 { rl.multisplit = rl.multisplit[1:] } else { rl.multisplit = []string{} } return string(rl.line), nil } s := string(r[:i]) if rl.evtKeyPress[s] != nil { rl.clearHelpers() ret := rl.evtKeyPress[s](s, rl.line, rl.pos) rl.clearLine() rl.line = append(ret.NewLine, []rune{}...) rl.updateHelpers() // rl.echo rl.pos = ret.NewPos if ret.ClearHelpers { rl.resetHelpers() } else { rl.updateHelpers() } if len(ret.InfoText) > 0 { rl.infoText = ret.InfoText rl.clearHelpers() rl.renderHelpers() } if !ret.ForwardKey { continue } if ret.CloseReadline { rl.clearHelpers() return string(rl.line), nil } } // Before anything: we can never be both in modeTabCompletion and compConfirmWait, // because we need to confirm before entering completion. If both are true, there // is a problem (at least, the user has escaped the confirm hint some way). if (rl.modeTabCompletion && rl.searchMode != HistoryFind) && rl.compConfirmWait { rl.compConfirmWait = false } switch b[0] { // Errors & Returns -------------------------------------------------------------------------------- case charCtrlC: if rl.modeTabCompletion { rl.resetVirtualComp(true) rl.resetHelpers() rl.renderHelpers() continue } rl.clearHelpers() return "", CtrlC case charEOF: // ctrl d if len(rl.line) == 0 { rl.clearHelpers() return "", EOF } if rl.modeTabFind { rl.backspaceTabFind() } else { if (rl.pos < len(rl.line)) { rl.deleteBackspace(true) } } // Clear screen case charCtrlL: print(seqClearScreen) print(seqCursorTopLeft) if rl.Multiline { fmt.Println(rl.mainPrompt) } print(seqClearScreenBelow) rl.resetInfoText() rl.getInfoText() rl.renderHelpers() // Line Editing ------------------------------------------------------------------------------------ case charCtrlU: if rl.modeTabCompletion { rl.resetVirtualComp(true) } // Delete everything from the beginning of the line to the cursor position rl.saveBufToRegister(rl.line[:rl.pos]) rl.deleteToBeginning() rl.resetHelpers() rl.updateHelpers() case charCtrlK: if rl.modeTabCompletion { rl.resetVirtualComp(true) } // Delete everything after the cursor position rl.saveBufToRegister(rl.line[rl.pos:]) rl.deleteToEnd() rl.resetHelpers() rl.updateHelpers() case charBackspace, charBackspace2: // When currently in history completion, we refresh and automatically // insert the first (filtered) candidate, virtually if rl.modeAutoFind && rl.searchMode == HistoryFind { rl.resetVirtualComp(true) rl.backspaceTabFind() // Then update the printing, with the new candidate rl.updateVirtualComp() rl.renderHelpers() rl.viUndoSkipAppend = true continue } // Normal completion search does only refresh the search pattern and the comps if rl.modeTabFind || rl.modeAutoFind { rl.backspaceTabFind() rl.viUndoSkipAppend = true } else { // Always cancel any virtual completion rl.resetVirtualComp(false) // Vim mode has different behaviors if rl.InputMode == Vim { if rl.modeViMode == VimInsert { rl.backspace(false) } else if rl.pos != 0 { rl.pos-- } rl.renderHelpers() continue } // Else emacs deletes a character rl.backspace(false) rl.renderHelpers() } // Emacs Bindings ---------------------------------------------------------------------------------- case charCtrlW: if rl.modeTabCompletion { rl.resetVirtualComp(false) } // This is only available in Insert mode if rl.modeViMode != VimInsert { continue } rl.saveToRegister(rl.viJumpB(tokeniseLine)) rl.viDeleteByAdjust(rl.viJumpB(tokeniseLine)) rl.updateHelpers() case charCtrlY: if rl.modeTabCompletion { rl.resetVirtualComp(false) } // paste after the cursor position rl.viUndoSkipAppend = true buffer := rl.pasteFromRegister() rl.insert(buffer) rl.updateHelpers() case charCtrlE: if rl.modeTabCompletion { rl.resetVirtualComp(false) } // This is only available in Insert mode if rl.modeViMode != VimInsert { continue } if len(rl.line) > 0 { rl.pos = len(rl.line) } rl.viUndoSkipAppend = true rl.updateHelpers() case charCtrlA: if rl.modeTabCompletion { rl.resetVirtualComp(false) } // This is only available in Insert mode if rl.modeViMode != VimInsert { continue } rl.viUndoSkipAppend = true rl.pos = 0 rl.updateHelpers() // Command History --------------------------------------------------------------------------------- // NOTE: The alternative history source is triggered by Alt+r, // but because this is a sequence, the alternative history code // trigger is in the below rl.escapeSeq(r) function. case charCtrlR: rl.resetVirtualComp(false) // For some modes only, if we are in vim Keys mode, // we toogle back to insert mode. For others, we return // without getting the completions. if rl.modeViMode != VimInsert { rl.modeViMode = VimInsert rl.computePrompt() } rl.mainHist = true // false before rl.searchMode = HistoryFind rl.modeAutoFind = true rl.modeTabCompletion = true rl.modeTabFind = true rl.updateTabFind([]rune{}) rl.viUndoSkipAppend = true // Tab Completion & Completion Search --------------------------------------------------------------- case charTab: // The user cannot show completions if currently in Vim Normal mode if rl.InputMode == Vim && rl.modeViMode != VimInsert { continue } // If we have asked for completions, already printed, and we want to move selection. if rl.modeTabCompletion && !rl.compConfirmWait { rl.tabCompletionSelect = true rl.moveTabCompletionHighlight(1, 0) rl.updateVirtualComp() rl.renderHelpers() rl.viUndoSkipAppend = true } else { // Else we might be asked to confirm printing (if too many suggestions), or not. rl.getTabCompletion() // If too many completions and no yet confirmed, ask user for completion // comps, lines := rl.getCompletionCount() // if ((lines > GetTermLength()) || (lines > rl.MaxTabCompleterRows)) && !rl.compConfirmWait { // sentence := fmt.Sprintf("%s show all %d completions (%d lines) ? tab to confirm", // FOREWHITE, comps, lines) // rl.promptCompletionConfirm(sentence) // continue // } rl.compConfirmWait = false rl.modeTabCompletion = true // Also here, if only one candidate is available, automatically // insert it and don't bother printing completions. // Quit the tab completion mode to avoid asking to the user // to press Enter twice to actually run the command. if rl.hasOneCandidate() { rl.insertCandidate() // Refresh first, and then quit the completion mode rl.updateHelpers() // REDUNDANT WITH getTabCompletion() rl.viUndoSkipAppend = true rl.resetTabCompletion() continue } rl.updateHelpers() // REDUNDANT WITH getTabCompletion() rl.viUndoSkipAppend = true continue } case charCtrlF: rl.resetVirtualComp(true) if !rl.modeTabCompletion { rl.modeTabCompletion = true } if rl.compConfirmWait { rl.resetHelpers() } // Both these settings apply to when we already // are in completion mode and when we are not. rl.searchMode = CompletionFind rl.modeAutoFind = true // Switch from history to completion search if rl.modeTabCompletion && rl.searchMode == HistoryFind { rl.searchMode = CompletionFind } rl.updateTabFind([]rune{}) rl.viUndoSkipAppend = true case charCtrlG: if rl.modeAutoFind && rl.searchMode == HistoryFind { rl.resetVirtualComp(false) rl.resetTabFind() rl.resetHelpers() rl.renderHelpers() continue } if rl.modeAutoFind { rl.resetTabFind() rl.resetHelpers() rl.renderHelpers() } case charCtrlUnderscore: rl.undoLast() rl.viUndoSkipAppend = true case '\r': fallthrough case '\n': if rl.modeTabCompletion { cur := rl.getCurrentGroup() // Check that there is a group indeed, as we might have no completions. if cur == nil { rl.clearHelpers() rl.resetTabCompletion() rl.renderHelpers() continue } // IF we have a prefix and completions printed, but no candidate // (in which case the completion is ""), we immediately return. completion := cur.getCurrentCell(rl) prefix := len(rl.tcPrefix) if prefix > len(completion) { rl.carridgeReturn() return string(rl.line), nil } // Else, we insert the completion candidate in the real input line. // By default we add a space, unless completion group asks otherwise. rl.compAddSpace = true rl.resetVirtualComp(false) // If we were in history completion, immediately execute the line. if rl.modeAutoFind && rl.searchMode == HistoryFind { rl.carridgeReturn() return string(rl.line), nil } // Reset completions and update input line rl.clearHelpers() rl.resetTabCompletion() rl.renderHelpers() continue } rl.carridgeReturn() return string(rl.line), nil // Vim -------------------------------------------------------------------------------------- case charEscape: // If we were waiting for completion confirm, abort if rl.compConfirmWait { rl.compConfirmWait = false rl.renderHelpers() } // We always refresh the completion candidates, except if we are currently // cycling through them, because then it would just append the candidate. if rl.modeTabCompletion { if string(r[:i]) != seqShiftTab && string(r[:i]) != seqForwards && string(r[:i]) != seqBackwards && string(r[:i]) != seqUp && string(r[:i]) != seqDown { rl.resetVirtualComp(false) } } // Once helpers of all sorts are cleared, we can process // the change of input modes, etc. rl.escapeSeq(r[:i]) // Dispatch -------------------------------------------------------------------------------------- default: // If we were waiting for completion confirm, abort if rl.compConfirmWait { rl.resetVirtualComp(false) rl.compConfirmWait = false rl.renderHelpers() } // When currently in history completion, we refresh and automatically // insert the first (filtered) candidate, virtually if rl.modeAutoFind && rl.searchMode == HistoryFind { rl.resetVirtualComp(true) rl.updateTabFind(r[:i]) rl.updateVirtualComp() rl.renderHelpers() rl.viUndoSkipAppend = true continue } // Not sure that CompletionFind is useful, nor one of the other two if rl.modeAutoFind || rl.modeTabFind { rl.resetVirtualComp(false) rl.updateTabFind(r[:i]) rl.viUndoSkipAppend = true } else { rl.resetVirtualComp(false) rl.editorInput(r[:i]) if len(rl.multiline) > 0 && rl.modeViMode == VimKeys { rl.skipStdinRead = true } } rl.clearHelpers() } rl.undoAppendHistory() } } // editorInput is an unexported function used to determine what mode of text // entry readline is currently configured for and then update the line entries // accordingly. func (rl *Instance) editorInput(r []rune) { switch rl.modeViMode { case VimKeys: rl.vi(r[0]) rl.refreshVimStatus() case VimDelete: rl.viDelete(r[0]) rl.refreshVimStatus() case VimReplaceOnce: rl.modeViMode = VimKeys rl.deleteX() rl.insert([]rune{r[0]}) rl.refreshVimStatus() case VimReplaceMany: for _, char := range r { if rl.pos != len(rl.line) { rl.deleteX() } rl.insert([]rune{char}) } rl.refreshVimStatus() default: // Don't insert control keys if r[0] >= 1 && r[0] <= 31 { return } // We reset the history nav counter each time we come here: // We don't need it when inserting text. rl.histNavIdx = 0 rl.insert(r) rl.writeHintText() } rl.echoRightPrompt() if len(rl.multisplit) == 0 { rl.syntaxCompletion() } } // viEscape - In case th user is using Vim input, and the escape sequence has not // been handled by other cases, we dispatch it to Vim and handle a few cases here. func (rl *Instance) viEscape(r []rune) { // Sometimes the escape sequence is interleaved with another one, // but key strokes might be in the wrong order, so we double check // and escape the mode only if needed. if rl.modeViMode == VimInsert && len(r) == 1 && r[0] == 27 { if len(rl.line) > 0 && rl.pos > 0 { rl.pos-- } rl.modeViMode = VimKeys rl.viIteration = "" rl.refreshVimStatus() return } } func (rl *Instance) escapeSeq(r []rune) { switch string(r) { // Vim escape sequences & dispatching -------------------------------------------------------- case string(charEscape): switch { case rl.modeAutoFind: rl.resetTabFind() rl.clearHelpers() rl.resetTabCompletion() rl.resetHelpers() rl.renderHelpers() case rl.modeTabFind: rl.resetTabFind() rl.resetTabCompletion() case rl.modeTabCompletion: rl.clearHelpers() rl.resetTabCompletion() rl.renderHelpers() default: // No matter the input mode, we exit // any completion confirm if there's one. if rl.compConfirmWait { rl.compConfirmWait = false rl.clearHelpers() rl.renderHelpers() return } // If we are in Vim mode, the escape key has its usage. // Otherwise in emacs mode the escape key does nothing. if rl.InputMode == Vim { rl.viEscape(r) return } // This refreshed and actually prints the new Vim status // if we have indeed change the Vim mode. rl.clearHelpers() rl.renderHelpers() } rl.viUndoSkipAppend = true // Tab completion movements ------------------------------------------------------------------ case seqShiftTab: if rl.modeTabCompletion && !rl.compConfirmWait { rl.tabCompletionReverse = true rl.moveTabCompletionHighlight(-1, 0) rl.updateVirtualComp() rl.tabCompletionReverse = false rl.renderHelpers() rl.viUndoSkipAppend = true return } case seqUp: if rl.modeTabCompletion { rl.tabCompletionSelect = true rl.tabCompletionReverse = true rl.moveTabCompletionHighlight(-1, 0) rl.updateVirtualComp() rl.tabCompletionReverse = false rl.renderHelpers() return } rl.mainHist = true rl.walkHistory(1) moveCursorForwards(len(rl.line) - rl.pos) rl.pos = len(rl.line) case seqDown: if rl.modeTabCompletion { rl.tabCompletionSelect = true rl.moveTabCompletionHighlight(1, 0) rl.updateVirtualComp() rl.renderHelpers() return } rl.mainHist = true rl.walkHistory(-1) moveCursorForwards(len(rl.line) - rl.pos) rl.pos = len(rl.line) case seqForwards: if rl.modeTabCompletion { rl.tabCompletionSelect = true rl.moveTabCompletionHighlight(1, 0) rl.updateVirtualComp() rl.renderHelpers() return } if (rl.modeViMode == VimInsert && rl.pos < len(rl.line)) || (rl.modeViMode != VimInsert && rl.pos < len(rl.line)-1) { rl.moveCursorByAdjust(1) } rl.updateHelpers() rl.viUndoSkipAppend = true case seqBackwards: if rl.modeTabCompletion { rl.tabCompletionSelect = true rl.tabCompletionReverse = true rl.moveTabCompletionHighlight(-1, 0) rl.updateVirtualComp() rl.tabCompletionReverse = false rl.renderHelpers() return } rl.moveCursorByAdjust(-1) rl.viUndoSkipAppend = true rl.updateHelpers() // Registers ------------------------------------------------------------------------------- case seqAltQuote: if rl.modeViMode != VimInsert { return } rl.modeTabCompletion = true rl.modeAutoFind = true rl.searchMode = RegisterFind // Else we might be asked to confirm printing (if too many suggestions), or not. rl.getTabCompletion() rl.viUndoSkipAppend = true rl.renderHelpers() // Movement ------------------------------------------------------------------------------- case seqCtrlLeftArrow: rl.moveCursorByAdjust(rl.viJumpB(tokeniseLine)) rl.updateHelpers() return case seqCtrlRightArrow: rl.insert(rl.hintText) rl.moveCursorByAdjust(rl.viJumpW(tokeniseLine)) rl.updateHelpers() return case seqDelete,seqDelete2: if rl.modeTabFind { rl.backspaceTabFind() } else { if (rl.pos < len(rl.line)) { rl.deleteBackspace(true) } } case seqHome, seqHomeSc: if rl.modeTabCompletion { return } rl.moveCursorByAdjust(-rl.pos) rl.updateHelpers() rl.viUndoSkipAppend = true case seqEnd, seqEndSc: if rl.modeTabCompletion { return } rl.moveCursorByAdjust(len(rl.line) - rl.pos) rl.updateHelpers() rl.viUndoSkipAppend = true case seqAltB: if rl.modeTabCompletion { return } // This is only available in Insert mode if rl.modeViMode != VimInsert { return } move := rl.emacsBackwardWord(tokeniseLine) rl.moveCursorByAdjust(-move) rl.updateHelpers() case seqAltF: if rl.modeTabCompletion { return } // This is only available in Insert mode if rl.modeViMode != VimInsert { return } move := rl.emacsForwardWord(tokeniseLine) rl.moveCursorByAdjust(move) rl.updateHelpers() case seqAltR: rl.resetVirtualComp(false) // For some modes only, if we are in vim Keys mode, // we toogle back to insert mode. For others, we return // without getting the completions. if rl.modeViMode != VimInsert { rl.modeViMode = VimInsert } rl.mainHist = false // true before rl.searchMode = HistoryFind rl.modeAutoFind = true rl.modeTabCompletion = true rl.modeTabFind = true rl.updateTabFind([]rune{}) rl.viUndoSkipAppend = true case seqAltBackspace: if rl.modeTabCompletion { rl.resetVirtualComp(false) } // This is only available in Insert mode if rl.modeViMode != VimInsert { return } rl.saveToRegister(rl.viJumpB(tokeniseLine)) rl.viDeleteByAdjust(rl.viJumpB(tokeniseLine)) rl.updateHelpers() case seqCtrlDelete, seqCtrlDelete2, seqAltD: if rl.modeTabCompletion { rl.resetVirtualComp(false) } rl.saveToRegister(rl.emacsForwardWord(tokeniseLine)) // vi delete, emacs forward, funny huh rl.viDeleteByAdjust(rl.emacsForwardWord(tokeniseLine)) rl.updateHelpers() case seqAltDelete: if rl.modeTabCompletion { rl.resetVirtualComp(false) } rl.saveToRegister(-rl.emacsBackwardWord(tokeniseLine)) rl.viDeleteByAdjust(-rl.emacsBackwardWord(tokeniseLine)) rl.updateHelpers() default: if rl.modeTabFind { return } // alt+numeric append / delete if len(r) == 2 && '1' <= r[1] && r[1] <= '9' { if rl.modeViMode == VimDelete { rl.viDelete(r[1]) return } line, err := rl.mainHistory.GetLine(rl.mainHistory.Len() - 1) if err != nil { return } if !rl.mainHist { line, err = rl.altHistory.GetLine(rl.altHistory.Len() - 1) if err != nil { return } } tokens, _, _ := tokeniseSplitSpaces([]rune(line), 0) pos := int(r[1]) - 48 // convert ASCII to integer if pos > len(tokens) { return } rl.insert([]rune(tokens[pos-1])) } else { rl.viUndoSkipAppend = true } } } func (rl *Instance) carridgeReturn() { rl.moveCursorByAdjust(len(rl.line)) rl.updateHelpers() rl.clearHelpers() print("\r\n") if rl.HistoryAutoWrite { var err error // Main history if rl.mainHistory != nil { rl.histPos, err = rl.mainHistory.Write(string(rl.line)) if err != nil { print(err.Error() + "\r\n") } } // Alternative history if rl.altHistory != nil { rl.histPos, err = rl.altHistory.Write(string(rl.line)) if err != nil { print(err.Error() + "\r\n") } } } } func isMultiline(r []rune) bool { for i := range r { if (r[i] == '\r' || r[i] == '\n') && i != len(r)-1 { return true } } return false } func (rl *Instance) allowMultiline(data []byte) bool { rl.clearHelpers() printf("\r\nWARNING: %d bytes of multiline data was dumped into the shell!", len(data)) for { print("\r\nDo you wish to proceed (yes|no|preview)? [y/n/p] ") b := make([]byte, 1024) i, err := os.Stdin.Read(b) if err != nil { return false } s := string(b[:i]) print(s) switch s { case "y", "Y": print("\r\n" + rl.mainPrompt) return true case "n", "N": print("\r\n" + rl.mainPrompt) return false case "p", "P": preview := string(bytes.Replace(data, []byte{'\r'}, []byte{'\r', '\n'}, -1)) if rl.SyntaxHighlighter != nil { preview = rl.SyntaxHighlighter([]rune(preview)) } print("\r\n" + preview) default: print("\r\nInvalid response. Please answer `y` (yes), `n` (no) or `p` (preview)") } } }