package historyimport ()// Sources manages and serves all history sources for the current shell.typeSourcesstruct {// Shell parameters line *core.Line cursor *core.Cursor hint *ui.Hint config *inputrc.Config// History sources list map[string]Source// Sources of history lines names []string// Names of histories stored in rl.histories maxEntries int// Inputrc configured maximum number of entries. sourcePos int// The index of the currently used history hpos int// Index used for navigating the history lines with arrows/j/k cpos int// A temporary cursor position used when searching/moving around.// Line changes history skip bool// Skip saving the current line state. undoing bool// The last command executed was an undo. last inputrc.Bind// The last command being ran. lines map[string]map[int]*lineHistory// Each line in each history source has its own buffer history.// Lines accepted infer bool// If the last command ran needs to infer the history line. accepted bool// The line has been accepted and must be returned. acceptHold bool// Should we reuse the same accepted line on the next loop. acceptLine core.Line// The line to return to the caller. acceptErr error// An error to return to the caller.}// NewSources is a required constructor for the history sources manager type.func ( *core.Line, *core.Cursor, *ui.Hint, *inputrc.Config) *Sources { := &Sources{// History sourceslist: make(map[string]Source),// Line historylines: make(map[string]map[int]*lineHistory),// Shell parametersline: ,cursor: ,cpos: -1,hpos: -1,hint: ,config: , } .names = append(.names, defaultSourceName) .list[defaultSourceName] = new(memory)// Inputrc settings. .maxEntries = .GetInt("history-size") := .GetString("history-size") != ""if .maxEntries == 0 && ! { .maxEntries = -1 } elseif .maxEntries == 0 && { .maxEntries = 500 }return}// Init initializes the history sources positions and buffers// at the start of each readline loop. If the last command asked// to infer a command line from the history, it is performed now.func ( *Sources) {deferfunc() { .accepted = false .acceptLine = nil .acceptErr = nil .cpos = -1 }()if .acceptHold { .hpos = -1 .line.Set(.acceptLine...) .cursor.Set(.line.Len())return }if !.infer { .hpos = -1 := .getHistoryLineChanges() [.hpos] = &lineHistory{}return }switch .hpos {case -1:case0: .InferNext()default: .Walk(-1) } .infer = false}// Add adds a source of history lines bound to a given name (printed above this source when used).// If the shell currently has only an in-memory (default) history source available, the call will// drop this source and replace it with the provided one. Following calls add to the list.func ( *Sources) ( string, Source) {iflen(.list) == 1 && .names[0] == defaultSourceName {delete(.list, defaultSourceName) .names = make([]string, 0) } .names = append(.names, ) .list[] = }// AddFromFile adds a command history source from a file path.// The name is used when using/searching the history source.func ( *Sources) (, string) { := new(fileHistory) .file = .lines, _ = openHist() .Add(, )}// Delete deletes one or more history source by name.// If no arguments are passed, all currently bound sources are removed.func ( *Sources) ( ...string) {iflen() == 0 { .list = make(map[string]Source) .names = make([]string, 0)return }for , := range {delete(.list, )for , := range .names {if == { .names = append(.names[:], .names[+1:]...)break } } } .sourcePos = 0if !.infer { .hpos = -1 }}// Walk goes to the next or previous history line in the active source.// If at the beginning of the history, the first history line is kept.// If at the end of it, the main input buffer and cursor position is restored.func ( *Sources) ( int) { := .Current()if == nil || .Len() == 0 {return }// Can't go back further than the first line.if .hpos == .Len() && == 1 {return }// Save the current line buffer if we are leaving it.if .hpos == -1 && > 0 { .skip = false .Save() .cpos = -1 .hpos = 0 } .hpos += switch {case .hpos < -1: .hpos = -1returncase .hpos == 0: .restoreLineBuffer()returncase .hpos > .Len(): .hpos = .Len() }varstringvarerror// When there is an available change history for // this line, use it instead of the fetched line.if := .getLineHistory(); != nil && len(.items) > 0 { = .items[len(.items)-1].line } elseif , = .GetLine(.Len() - .hpos); != nil { .hint.Set(color.FgRed + "history error: " + .Error())return }// Update line buffer and cursor position. .setLineCursorMatch()}// Fetch fetches the history event at the provided// index position and makes it the current buffer.func ( *Sources) ( int) { := .Current()if == nil || .Len() == 0 {return }if < 0 || >= .Len() {return } , := .GetLine()if != nil { .hint.Set(color.FgRed + "history error: " + .Error())return } .setLineCursorMatch()}// GetLast returns the last saved history line in the active history source.func ( *Sources) () string { := .Current()if == nil || .Len() == 0 {return"" } , := .GetLine(.Len() - 1)if != nil {return"" }return}// Cycle checks for the next history source (if any) and makes it the active one.// The active one is used in completions, and all history-related commands.// If next is false, the engine cycles to the previous source.func ( *Sources) ( bool) {switch {casetrue: .sourcePos++if .sourcePos == len(.names) { .sourcePos = 0 }casefalse: .sourcePos--if .sourcePos < 0 { .sourcePos = len(.names) - 1 } }}// OnLastSource returns true if the currently active// history source is the last one in the list.func ( *Sources) () bool {return .sourcePos == len(.names)-1}// Current returns the current/active history source.func ( *Sources) () Source {iflen(.list) == 0 {returnnil }return .list[.names[.sourcePos]]}// Write writes the accepted input line to all available sources.// If infer is true, the next history initialization will automatically insert the next// history line event after the first match of the line, which one is then NOT written.func ( *Sources) ( bool) {if { .infer = truereturn } := string(*.line)iflen(strings.TrimSpace()) == 0 {return }for , := range .list {if == nil {continue }// Don't write it if the history source has reached // the maximum number of lines allowed (inputrc)if .maxEntries == 0 || .maxEntries >= .Len() {continue }varerror// Don't write the line if it's identical to the last one. , := .GetLine(.Len() - 1)if == nil && != "" && strings.TrimSpace() == strings.TrimSpace() {return }// Save the line and notify through hints if an error raised. _, = .Write()if != nil { .hint.Set(color.FgRed + .Error()) } }}// Accept is used to signal the line has been accepted by the user and must be// returned to the readline caller. If hold is true, the line is preserved// and redisplayed on the next loop. If infer, the line is not written to// the history, but preserved as a line to match against on the next loop.// If infer is false, the line is automatically written to active sources.func ( *Sources) (, bool, error) { .accepted = true .acceptHold = .acceptLine = *.line .acceptErr = // Write the line to the history sources only when the line is not // returned along with an error (generally, a CtrlC/CtrlD keypress).if == nil { .Write() }}// LineAccepted returns true if the user has accepted the line, signaling// that the shell must return from its loop. The error can be nil, but may// indicate a CtrlC/CtrlD style error.func ( *Sources) () (bool, string, error) {if !.accepted {returnfalse, "", nil } := string(.acceptLine)// Revert all state changes to all lines.if .config.GetBool("revert-all-at-newline") {for := range .lines { .lines[] = make(map[int]*lineHistory) } }returntrue, , .acceptErr}// InsertMatch replaces the buffer with the first history line matching the// provided buffer, either as a substring (if regexp is true), or as a prefix.// If the line argument is nil, the current line buffer is used to match against.func ( *Sources) ( *core.Line, *core.Cursor, , , bool) {iflen(.list) == 0 {return }if .Current() == nil {return }// When the provided line is empty, we must use // the last known state of the main input line. , = .getLine(, ) := .Pos() != 0// Don't go back to the beginning of // history if we are at the end of it.if && .hpos <= -1 { .hpos = -1return } , , := .match(, , , , )// If no match was found, return anyway, but if we were going forward // (down to the current input line), reinstore the main line buffer.if ! {if { .hpos = -1 .Undo() }return }// Update the line/cursor, and save the history position .hpos = .Current().Len() - .line.Set([]rune()...)if { .cursor.Set(.Pos()) } else { .cursor.Set(.line.Len()) }}// InferNext finds a line matching the current line in the history,// then finds the line event following it and, if any, inserts it.func ( *Sources) () {iflen(.list) == 0 {return } := .Current()if == nil {return } , , := .match(.line, nil, false, false, false)if ! {return }// If we have no match we return, or check for the next line.if .Len() <= (.Len()-)+1 {return }// Insert the next line , := .GetLine( + 1)if != nil {return } .line.Set([]rune()...) .cursor.Set(.line.Len())}// Suggest returns the first line matching the current line buffer,// so that caller can use for things like history autosuggestion.// If no line matches the current line, it will return the latter.func ( *Sources) ( *core.Line) core.Line {iflen(.list) == 0 || len(*) == 0 {return * }if .Current() == nil {return * } , , := .match(, nil, false, false, false)if ! {return * }returncore.Line([]rune())}// Complete returns completions with the current history source values.// If forward is true, the completions are proposed from the most ancient// line in the history source to the most recent. If filter is true,// only lines that match the current input line as a prefix are given.func ( *Sources, , bool, int, *regexp.Regexp) completion.Values {iflen(.list) == 0 {returncompletion.Values{} } := .Current()if == nil {returncompletion.Values{} } .hint.Set(color.Bold + color.FgCyanBright + .names[.sourcePos] + color.Reset) := make([]completion.Candidate, 0) := make([]string, 0)// Set up iteration clausesvar (intfunc( int) boolfunc( int) int )if { = -1 = func( int) bool { return < .Len()-1 && >= 0 } = func( int) int { return + 1 } } else { = .Len() = func( int) bool { return > 0 && >= 0 } = func( int) int { return - 1 } }// And generate the completions.for () { = () , := .GetLine()if != nil {continue }ifstrings.TrimSpace() == "" {continue }if && !strings.HasPrefix(, string(*.line)) {continue } elseif != nil && !.MatchString() {continue }// If this history line is a duplicate of an existing one, // remove the existing one and keep this one as it's more recent.if , := contains(, ); { = append([:], [+1:]...) = append(, )continue }// Add to the list of printed lines if we have a new one. = append(, ) := strings.ReplaceAll(, "\n", ` `)// Proper pad for indexes := strconv.Itoa() := strings.Repeat(" ", len(strconv.Itoa(.Len()))-len()) = fmt.Sprintf("%s%s %s%s", color.Dim, +, color.DimReset, ) := completion.Candidate{Display: ,Value: , } = append(, ) -- } := completion.AddRaw() .NoSort["*"] = true .ListLong["*"] = true .PREFIX = string(*.line)return}// Name returns the name of the currently active history source.func ( *Sources) () string {return .names[.sourcePos]}func ( *Sources) ( *core.Line, *core.Cursor, , , bool) ( string, int, bool) {iflen(.list) == 0 {return , , } := .Current()if == nil {return , , }// Set up iteration clausesvarintvarfunc( int) boolvarfunc( int) intif { = -1 = func( int) bool { return < .Len() } = func( int) int { return + 1 } } else { = .Len() = func( int) bool { return > 0 } = func( int) int { return - 1 } }if && .hpos > -1 { = .Len() - .hpos }for () {// Fetch the next/prev line and adapt its length. = () , := .GetLine()if != nil {return , , } := string(*)if != nil && .Pos() < .Len() { = [:.Pos()] }// Matching: either as substring (regex) or since beginning.switch {case : , := regexp.Compile(regexp.QuoteMeta())if != nil {continue }// Go to next line if not matching as a substring.if !.MatchString() {continue }default:// If too short or if not fully matchingiflen() < len() || (len() > 0 && !strings.HasPrefix(, )) {continue } }// Else we have our history match.return , , true }// We should have returned a match from the loop.return"", 0, false}// use the "main buffer" and its cursor if no line/cursor has been provided to match against.func ( *Sources) ( *core.Line, *core.Cursor) (*core.Line, *core.Cursor) {if .hpos == -1 { := .skip .skip = false .Save() .skip = }if == nil { = new(core.Line) = core.NewCursor() := .getHistoryLineChanges()if == nil {return , } := [0]if == nil || len(.items) == 0 {return , } := .items[len(.items)-1] .Set([]rune(.line)...) .Set(.pos) }if == nil { = core.NewCursor() }return , }// when walking down/up the lines (not search-matching them),// this updates the buffer and the cursor position.func ( *Sources) ( string) {// Save the current cursor position when not saved before.if .cpos == -1 && .line.Len() > 0 && .cursor.Pos() <= .line.Len() { .cpos = .cursor.Pos() } .line.Set([]rune()...)// Set cursor depending on inputrc options and line length.if .config.GetBool("history-preserve-point") && .line.Len() > .cpos && .cpos != -1 { .cursor.Set(.cpos) } else { .cursor.Set(.line.Len()) }}func contains( []string, string) (bool, int) {for , := range {if == {returntrue, } }returnfalse, 0}func removeDuplicates( []string) []string { := []string{}for , := range {if , := contains(, ); { = append(, ) } }return}
The pages are generated with Goldsv0.8.2. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.