src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/cli/modes/lastcmd.go (about) 1 package modes 2 3 import ( 4 "fmt" 5 "strconv" 6 "strings" 7 8 "src.elv.sh/pkg/cli" 9 "src.elv.sh/pkg/cli/histutil" 10 "src.elv.sh/pkg/cli/tk" 11 "src.elv.sh/pkg/ui" 12 ) 13 14 // Lastcmd is a mode for inspecting the last command, and inserting part of all 15 // of it. It is based on the ComboBox widget. 16 type Lastcmd interface { 17 tk.ComboBox 18 } 19 20 // LastcmdSpec specifies the configuration for the lastcmd mode. 21 type LastcmdSpec struct { 22 // Key bindings. 23 Bindings tk.Bindings 24 // Store provides the source for the last command. 25 Store LastcmdStore 26 // Wordifier breaks a command into words. 27 Wordifier func(string) []string 28 } 29 30 // LastcmdStore is a subset of histutil.Store used in lastcmd mode. 31 type LastcmdStore interface { 32 Cursor(prefix string) histutil.Cursor 33 } 34 35 var _ = LastcmdStore(histutil.Store(nil)) 36 37 // NewLastcmd creates a new lastcmd mode. 38 func NewLastcmd(app cli.App, cfg LastcmdSpec) (Lastcmd, error) { 39 codeArea, err := FocusedCodeArea(app) 40 if err != nil { 41 return nil, err 42 } 43 if cfg.Store == nil { 44 return nil, errNoHistoryStore 45 } 46 c := cfg.Store.Cursor("") 47 c.Prev() 48 cmd, err := c.Get() 49 if err != nil { 50 return nil, fmt.Errorf("db error: %v", err) 51 } 52 wordifier := cfg.Wordifier 53 if wordifier == nil { 54 wordifier = strings.Fields 55 } 56 cmdText := cmd.Text 57 words := wordifier(cmdText) 58 entries := make([]lastcmdEntry, len(words)+1) 59 entries[0] = lastcmdEntry{content: cmdText} 60 for i, word := range words { 61 entries[i+1] = lastcmdEntry{strconv.Itoa(i), strconv.Itoa(i - len(words)), word} 62 } 63 64 accept := func(text string) { 65 codeArea.MutateState(func(s *tk.CodeAreaState) { 66 s.Buffer.InsertAtDot(text) 67 }) 68 app.PopAddon() 69 } 70 w := tk.NewComboBox(tk.ComboBoxSpec{ 71 CodeArea: tk.CodeAreaSpec{Prompt: modePrompt(" LASTCMD ", true)}, 72 ListBox: tk.ListBoxSpec{ 73 Bindings: cfg.Bindings, 74 OnAccept: func(it tk.Items, i int) { 75 accept(it.(lastcmdItems).entries[i].content) 76 }, 77 }, 78 OnFilter: func(w tk.ComboBox, p string) { 79 items := filterLastcmdItems(entries, p) 80 if len(items.entries) == 1 { 81 accept(items.entries[0].content) 82 } else { 83 w.ListBox().Reset(items, 0) 84 } 85 }, 86 }) 87 return w, nil 88 } 89 90 type lastcmdItems struct { 91 negFilter bool 92 entries []lastcmdEntry 93 } 94 95 type lastcmdEntry struct { 96 posIndex string 97 negIndex string 98 content string 99 } 100 101 func filterLastcmdItems(allEntries []lastcmdEntry, p string) lastcmdItems { 102 if p == "" { 103 return lastcmdItems{false, allEntries} 104 } 105 var entries []lastcmdEntry 106 negFilter := strings.HasPrefix(p, "-") 107 for _, entry := range allEntries { 108 if (negFilter && strings.HasPrefix(entry.negIndex, p)) || 109 (!negFilter && strings.HasPrefix(entry.posIndex, p)) { 110 entries = append(entries, entry) 111 } 112 } 113 return lastcmdItems{negFilter, entries} 114 } 115 116 func (it lastcmdItems) Show(i int) ui.Text { 117 index := "" 118 entry := it.entries[i] 119 if it.negFilter { 120 index = entry.negIndex 121 } else { 122 index = entry.posIndex 123 } 124 // NOTE: We now use a hardcoded width of 3 for the index, which will work as 125 // long as the command has less than 1000 words (when filter is positive) or 126 // 100 words (when filter is negative). 127 return ui.T(fmt.Sprintf("%3s %s", index, entry.content)) 128 } 129 130 func (it lastcmdItems) Len() int { return len(it.entries) }