package cointop import ( "regexp" "strings" "github.com/cointop-sh/cointop/pkg/levenshtein" "github.com/cointop-sh/cointop/pkg/ui" log "github.com/sirupsen/logrus" ) // SearchFieldView is structure for search field view type SearchFieldView = ui.View // NewSearchFieldView returns a new search field view func NewSearchFieldView() *SearchFieldView { return ui.NewView("searchfield") } // InputView is structure for help view type InputView = ui.View // NewInputView returns a new help view func NewInputView() *InputView { return ui.NewView("input") } // OpenSearch opens the search field func (ct *Cointop) OpenSearch() error { log.Debug("OpenSearch()") if ct.ui.ActiveViewName() != ct.Views.Table.Name() { return nil } ct.State.searchFieldVisible = true ct.ui.SetCursor(true) ct.SetActiveView(ct.Views.SearchField.Name()) return nil } // CancelSearch closes the search field func (ct *Cointop) CancelSearch() error { log.Debug("CancelSearch()") ct.State.searchFieldVisible = false ct.ui.SetCursor(false) ct.SetActiveView(ct.Views.Table.Name()) return nil } // DoSearch triggers the search and sets views func (ct *Cointop) DoSearch() error { log.Debug("DoSearch()") ct.Views.SearchField.Rewind() b := make([]byte, 100) n, err := ct.Views.SearchField.Read(b) if err != nil { return err } // TODO: do this a better way (SoC) ct.SetSelectedView(CoinsView) defer ct.SetActiveView(ct.Views.Table.Name()) if err != nil { return nil } if n == 0 { return nil } q := strings.TrimSpace(string(b[:n])) // remove slash regex := regexp.MustCompile(`/(.*)`) matches := regex.FindStringSubmatch(q) if len(matches) > 0 { q = matches[1] } return ct.Search(q) } // Search performs the search and filtering func (ct *Cointop) Search(q string) error { log.Debugf("Search(%s)", q) // If there are no coins, return no result if len(ct.State.coins) == 0 { return nil } // If search term is empty, use the previous search term. q = strings.TrimSpace(strings.ToLower(q)) if q == "" { q = ct.State.lastSearchQuery } else { ct.State.lastSearchQuery = q } canSearchSymbol := true canSearchName := true if strings.HasPrefix(q, "s:") { canSearchSymbol = true canSearchName = false q = q[2:] log.Debug("Search, by keyword") } if strings.HasPrefix(q, "n:") { canSearchSymbol = false canSearchName = true q = q[2:] log.Debug("Search, by name") } idx := -1 min := -1 var hasprefixidx []int var hasprefixdist []int // Start the search from the current position (+1), looking names that start with the search term, or symbols that match completely currentIndex := ct.GetGlobalCoinIndex(ct.HighlightedRowCoin()) + 1 if ct.IsLastPage() && ct.IsLastRow() { currentIndex = 0 } for i := currentIndex; i < len(ct.State.allCoins); i++ { coin := ct.State.allCoins[i] name := strings.ToLower(coin.Name) symbol := strings.ToLower(coin.Symbol) // if query matches symbol, return immediately if canSearchSymbol && symbol == q { ct.GoToGlobalIndex(i) return nil } if !canSearchName { continue } // if query matches name, return immediately if name == q { ct.GoToGlobalIndex(i) return nil } // store index with the smallest levenshtein dist := levenshtein.DamerauLevenshteinDistance(name, q) if min == -1 || dist <= min { idx = i min = dist } // store index where query is substring to name if strings.HasPrefix(name, q) { if len(hasprefixdist) == 0 || dist < hasprefixdist[0] { hasprefixidx = append(hasprefixidx, i) hasprefixdist = append(hasprefixdist, dist) } } } if !canSearchName { return nil } // go to row if prefix match if len(hasprefixidx) > 0 && hasprefixidx[0] != -1 && min > 0 { ct.GoToGlobalIndex(hasprefixidx[0]) return nil } // go to row if levenshtein distance is small enough if idx > -1 && min <= 6 { ct.GoToGlobalIndex(idx) return nil } return nil }