package readline import ( "bufio" "container/list" "fmt" "os" "strings" "sync" "github.com/ergochat/readline/internal/runes" ) type hisItem struct { Source []rune Version int64 Tmp []rune } func (h *hisItem) Clean() { h.Source = nil h.Tmp = nil } type opHistory struct { operation *operation history *list.List historyVer int64 current *list.Element fd *os.File fdLock sync.Mutex enable bool } func newOpHistory(operation *operation) (o *opHistory) { o = &opHistory{ operation: operation, history: list.New(), enable: true, } o.initHistory() return o } func (o *opHistory) isEnabled() bool { return o.enable && o.operation.GetConfig().HistoryLimit > 0 } func (o *opHistory) Reset() { o.history = list.New() o.current = nil } func (o *opHistory) initHistory() { cfg := o.operation.GetConfig() if cfg.HistoryFile != "" { o.historyUpdatePath(cfg) } } // only called by newOpHistory func (o *opHistory) historyUpdatePath(cfg *Config) { o.fdLock.Lock() defer o.fdLock.Unlock() f, err := os.OpenFile(cfg.HistoryFile, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) if err != nil { return } o.fd = f r := bufio.NewReader(o.fd) total := 0 for ; ; total++ { line, err := r.ReadString('\n') if err != nil { break } // ignore the empty line line = strings.TrimSpace(line) if len(line) == 0 { continue } o.Push([]rune(line)) o.Compact() } if total > cfg.HistoryLimit { o.rewriteLocked() } o.historyVer++ o.Push(nil) return } func (o *opHistory) Compact() { cfg := o.operation.GetConfig() for o.history.Len() > cfg.HistoryLimit && o.history.Len() > 0 { o.history.Remove(o.history.Front()) } } func (o *opHistory) Rewrite() { o.fdLock.Lock() defer o.fdLock.Unlock() o.rewriteLocked() } func (o *opHistory) rewriteLocked() { cfg := o.operation.GetConfig() if cfg.HistoryFile == "" { return } tmpFile := cfg.HistoryFile + ".tmp" fd, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_APPEND, 0666) if err != nil { return } buf := bufio.NewWriter(fd) for elem := o.history.Front(); elem != nil; elem = elem.Next() { buf.WriteString(string(elem.Value.(*hisItem).Source) + "\n") } buf.Flush() // replace history file if err = os.Rename(tmpFile, cfg.HistoryFile); err != nil { fd.Close() return } if o.fd != nil { o.fd.Close() } // fd is write only, just satisfy what we need. o.fd = fd } func (o *opHistory) Close() { o.fdLock.Lock() defer o.fdLock.Unlock() if o.fd != nil { o.fd.Close() } } func (o *opHistory) FindBck(isNewSearch bool, rs []rune, start int) (int, *list.Element) { for elem := o.current; elem != nil; elem = elem.Prev() { item := o.showItem(elem.Value) if isNewSearch { start += len(rs) } if elem == o.current { if len(item) >= start { item = item[:start] } } idx := runes.IndexAllBckEx(item, rs, o.operation.GetConfig().HistorySearchFold) if idx < 0 { continue } return idx, elem } return -1, nil } func (o *opHistory) FindFwd(isNewSearch bool, rs []rune, start int) (int, *list.Element) { for elem := o.current; elem != nil; elem = elem.Next() { item := o.showItem(elem.Value) if isNewSearch { start -= len(rs) if start < 0 { start = 0 } } if elem == o.current { if len(item)-1 >= start { item = item[start:] } else { continue } } idx := runes.IndexAllEx(item, rs, o.operation.GetConfig().HistorySearchFold) if idx < 0 { continue } if elem == o.current { idx += start } return idx, elem } return -1, nil } func (o *opHistory) showItem(obj interface{}) []rune { item := obj.(*hisItem) if item.Version == o.historyVer { return item.Tmp } return item.Source } func (o *opHistory) Prev() []rune { if o.current == nil { return nil } current := o.current.Prev() if current == nil { return nil } o.current = current return runes.Copy(o.showItem(current.Value)) } func (o *opHistory) Next() ([]rune, bool) { if o.current == nil { return nil, false } current := o.current.Next() if current == nil { return nil, false } o.current = current return runes.Copy(o.showItem(current.Value)), true } // Disable the current history func (o *opHistory) Disable() { o.enable = false } // Enable the current history func (o *opHistory) Enable() { o.enable = true } func (o *opHistory) debug() { debugPrint("-------") for item := o.history.Front(); item != nil; item = item.Next() { debugPrint(fmt.Sprintf("%+v", item.Value)) } } // save history func (o *opHistory) New(current []rune) (err error) { if !o.isEnabled() { return nil } current = runes.Copy(current) // if just use last command without modify // just clean lastest history if back := o.history.Back(); back != nil { prev := back.Prev() if prev != nil { if runes.Equal(current, prev.Value.(*hisItem).Source) { o.current = o.history.Back() o.current.Value.(*hisItem).Clean() o.historyVer++ return nil } } } if len(current) == 0 { o.current = o.history.Back() if o.current != nil { o.current.Value.(*hisItem).Clean() o.historyVer++ return nil } } if o.current != o.history.Back() { // move history item to current command currentItem := o.current.Value.(*hisItem) // set current to last item o.current = o.history.Back() current = runes.Copy(currentItem.Tmp) } // err only can be a IO error, just report err = o.Update(current, true) // push a new one to commit current command o.historyVer++ o.Push(nil) return } func (o *opHistory) Revert() { o.historyVer++ o.current = o.history.Back() } func (o *opHistory) Update(s []rune, commit bool) (err error) { if !o.isEnabled() { return nil } o.fdLock.Lock() defer o.fdLock.Unlock() s = runes.Copy(s) if o.current == nil { o.Push(s) o.Compact() return } r := o.current.Value.(*hisItem) r.Version = o.historyVer if commit { r.Source = s if o.fd != nil { // just report the error _, err = o.fd.Write([]byte(string(r.Source) + "\n")) } } else { r.Tmp = append(r.Tmp[:0], s...) } o.current.Value = r o.Compact() return } func (o *opHistory) Push(s []rune) { s = runes.Copy(s) elem := o.history.PushBack(&hisItem{Source: s}) o.current = elem }