github.com/elves/elvish@v0.15.0/pkg/cli/addons/histlist/histlist.go (about) 1 // Package histlist implements the history listing addon. 2 package histlist 3 4 import ( 5 "fmt" 6 "strings" 7 8 "github.com/elves/elvish/pkg/cli" 9 "github.com/elves/elvish/pkg/cli/histutil" 10 "github.com/elves/elvish/pkg/store" 11 "github.com/elves/elvish/pkg/ui" 12 ) 13 14 // Config contains configurations to start history listing. 15 type Config struct { 16 // Binding provides key binding. 17 Binding cli.Handler 18 // Store provides the source of all commands. 19 Store Store 20 // Dedup is called to determine whether deduplication should be done. 21 // Defaults to true if unset. 22 Dedup func() bool 23 // CaseSensitive is called to determine whether the filter should be 24 // case-sensitive. Defaults to true if unset. 25 CaseSensitive func() bool 26 } 27 28 // Store wraps the AllCmds method. It is a subset of histutil.Store. 29 type Store interface { 30 AllCmds() ([]store.Cmd, error) 31 } 32 33 var _ = Store(histutil.Store(nil)) 34 35 // Start starts history listing. 36 func Start(app cli.App, cfg Config) { 37 if cfg.Store == nil { 38 app.Notify("no history store") 39 return 40 } 41 if cfg.Dedup == nil { 42 cfg.Dedup = func() bool { return true } 43 } 44 if cfg.CaseSensitive == nil { 45 cfg.CaseSensitive = func() bool { return true } 46 } 47 48 cmds, err := cfg.Store.AllCmds() 49 if err != nil { 50 app.Notify("db error: " + err.Error()) 51 } 52 last := map[string]int{} 53 for i, cmd := range cmds { 54 last[cmd.Text] = i 55 } 56 cmdItems := items{cmds, last} 57 58 w := cli.NewComboBox(cli.ComboBoxSpec{ 59 CodeArea: cli.CodeAreaSpec{Prompt: func() ui.Text { 60 content := " HISTORY " 61 if cfg.Dedup() { 62 content += "(dedup on) " 63 } 64 if !cfg.CaseSensitive() { 65 content += "(case-insensitive) " 66 } 67 return cli.ModeLine(content, true) 68 }}, 69 ListBox: cli.ListBoxSpec{ 70 OverlayHandler: cfg.Binding, 71 OnAccept: func(it cli.Items, i int) { 72 text := it.(items).entries[i].Text 73 app.CodeArea().MutateState(func(s *cli.CodeAreaState) { 74 buf := &s.Buffer 75 if buf.Content == "" { 76 buf.InsertAtDot(text) 77 } else { 78 buf.InsertAtDot("\n" + text) 79 } 80 }) 81 app.MutateState(func(s *cli.State) { s.Addon = nil }) 82 }, 83 }, 84 OnFilter: func(w cli.ComboBox, p string) { 85 it := cmdItems.filter(p, cfg.Dedup(), cfg.CaseSensitive()) 86 w.ListBox().Reset(it, it.Len()-1) 87 }, 88 }) 89 90 app.MutateState(func(s *cli.State) { s.Addon = w }) 91 app.Redraw() 92 } 93 94 type items struct { 95 entries []store.Cmd 96 last map[string]int 97 } 98 99 func (it items) filter(p string, dedup, caseSensitive bool) items { 100 if p == "" && !dedup { 101 return it 102 } 103 if !caseSensitive { 104 p = strings.ToLower(p) 105 } 106 var filtered []store.Cmd 107 for i, entry := range it.entries { 108 text := entry.Text 109 if dedup && it.last[text] != i { 110 continue 111 } 112 if !caseSensitive { 113 text = strings.ToLower(text) 114 } 115 if strings.Contains(text, p) { 116 filtered = append(filtered, entry) 117 } 118 } 119 return items{filtered, nil} 120 } 121 122 func (it items) Show(i int) ui.Text { 123 entry := it.entries[i] 124 // TODO: The alignment of the index works up to 10000 entries. 125 return ui.T(fmt.Sprintf("%4d %s", entry.Seq, entry.Text)) 126 } 127 128 func (it items) Len() int { return len(it.entries) }