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) }