Files
aerc-fork-mirror/commands/util.go
Robin Jarry 2dfeb7130a completion: refactor filter list api
Remove CompletionFromList which is a trivial wrapper around FilterList.

Remove the prefix, suffix and isFuzzy arguments from FilterList.

Replace prefix, suffix by an optional callback to allow post processing
of completion results before presenting them to the user.

Signed-off-by: Robin Jarry <robin@jarry.cc>
Tested-by: Inwit <inwit@sindominio.net>
2023-11-02 11:59:39 +01:00

259 lines
5.6 KiB
Go

package commands
import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"
"github.com/lithammer/fuzzysearch/fuzzy"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/xdg"
"git.sr.ht/~rjarry/aerc/log"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
"git.sr.ht/~rjarry/go-opt"
"github.com/gdamore/tcell/v2"
)
// QuickTerm is an ephemeral terminal for running a single command and quitting.
func QuickTerm(args []string, stdin io.Reader) (*app.Terminal, error) {
cmd := exec.Command(args[0], args[1:]...)
pipe, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
term, err := app.NewTerminal(cmd)
if err != nil {
return nil, err
}
term.OnClose = func(err error) {
if err != nil {
app.PushError(err.Error())
// remove the tab on error, otherwise it gets stuck
app.RemoveTab(term, false)
} else {
app.PushStatus("Process complete, press any key to close.",
10*time.Second)
term.OnEvent = func(event tcell.Event) bool {
app.RemoveTab(term, true)
return true
}
}
}
term.OnStart = func() {
status := make(chan error, 1)
go func() {
defer log.PanicHandler()
_, err := io.Copy(pipe, stdin)
defer pipe.Close()
status <- err
}()
err := <-status
if err != nil {
app.PushError(err.Error())
}
}
return term, nil
}
// CompletePath provides filesystem completions given a starting path.
func CompletePath(path string) []string {
if path == "" {
// default to cwd
cwd, err := os.Getwd()
if err != nil {
return nil
}
path = cwd
}
// strip trailing slashes, etc.
path = filepath.Clean(xdg.ExpandHome(path))
if _, err := os.Stat(path); os.IsNotExist(err) {
// if the path doesn't exist, it is likely due to it being a partial path
// in this case, we want to return possible matches (ie /hom* should match
// /home)
matches, err := filepath.Glob(fmt.Sprintf("%s*", path))
if err != nil {
return nil
}
if !strings.HasPrefix(path, ".") && !strings.Contains(path, "/.") {
log.Tracef("removing hidden files from glob results")
for i := len(matches) - 1; i >= 0; i-- {
if strings.HasPrefix(filepath.Base(matches[i]), ".") {
if i == len(matches)-1 {
matches = matches[:i]
continue
}
matches = append(matches[:i], matches[i+1:]...)
}
}
}
for i, m := range matches {
if isDir(m) {
m += "/"
}
matches[i] = opt.QuoteArg((xdg.TildeHome(m)))
}
sort.Strings(matches)
return matches
}
files := listDir(path, false)
for i, f := range files {
f = filepath.Join(path, f)
if isDir(f) {
f += "/"
}
files[i] = opt.QuoteArg((xdg.TildeHome(f)))
}
sort.Strings(files)
return files
}
func isDir(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return info.IsDir()
}
// return all filenames in a directory, optionally including hidden files
func listDir(path string, hidden bool) []string {
f, err := os.Open(path)
if err != nil {
return []string{}
}
files, err := f.Readdirnames(-1) // read all dir names
if err != nil {
return []string{}
}
if hidden {
return files
}
var filtered []string
for _, g := range files {
if !strings.HasPrefix(g, ".") {
filtered = append(filtered, g)
}
}
return filtered
}
// MarkedOrSelected returns either all marked messages if any are marked or the
// selected message instead
func MarkedOrSelected(pm app.ProvidesMessages) ([]uint32, error) {
// marked has priority over the selected message
marked, err := pm.MarkedMessages()
if err != nil {
return nil, err
}
if len(marked) > 0 {
return marked, nil
}
msg, err := pm.SelectedMessage()
if err != nil {
return nil, err
}
return []uint32{msg.Uid}, nil
}
// UidsFromMessageInfos extracts a uid slice from a slice of MessageInfos
func UidsFromMessageInfos(msgs []*models.MessageInfo) []uint32 {
uids := make([]uint32, len(msgs))
i := 0
for _, msg := range msgs {
uids[i] = msg.Uid
i++
}
return uids
}
func MsgInfoFromUids(store *lib.MessageStore, uids []uint32, statusInfo func(string)) ([]*models.MessageInfo, error) {
infos := make([]*models.MessageInfo, len(uids))
needHeaders := make([]uint32, 0)
for i, uid := range uids {
var ok bool
infos[i], ok = store.Messages[uid]
if !ok {
return nil, fmt.Errorf("uid not found")
}
if infos[i] == nil {
needHeaders = append(needHeaders, uid)
}
}
if len(needHeaders) > 0 {
store.FetchHeaders(needHeaders, func(msg types.WorkerMessage) {
var info string
switch m := msg.(type) {
case *types.Done:
info = "All headers fetched. Please repeat command."
case *types.Error:
info = fmt.Sprintf("Encountered error while fetching headers: %v", m.Error)
}
if statusInfo != nil {
statusInfo(info)
}
})
return nil, fmt.Errorf("Fetching missing message headers. Please wait.")
}
return infos, nil
}
func QuoteSpace(s string) string {
return opt.QuoteArg(s) + " "
}
// FilterList takes a list of valid completions and filters it, either
// by case smart prefix, or by fuzzy matching
// An optional post processing function can be passed to prepend, append or
// quote each value.
func FilterList(
valid []string, search string, postProc func(string) string,
) []string {
if postProc == nil {
postProc = opt.QuoteArg
}
out := make([]string, 0, len(valid))
if app.SelectedAccountUiConfig().FuzzyComplete {
for _, v := range fuzzy.RankFindFold(search, valid) {
out = append(out, postProc(v.Target))
}
} else {
for _, v := range valid {
if hasCaseSmartPrefix(v, search) {
out = append(out, postProc(v))
}
}
}
return out
}