mirror of
https://git.sr.ht/~rjarry/aerc
synced 2025-11-25 17:14:06 +01:00
go-opt v2.2.0 now supports providing individual descriptions for
completion items by appending a newline character to the completion item
followed by its description.
Do that for patch sub commands. Here's what it looks like:
apply (Apply the selected message(s) to the current project.)
cd (Change aerc's working directory to the current project.)
drop (Drop a patch from the repository.)
find (Search for applied patches.)
init (Create a new project.)
list (List the current project with the tracked patch sets.)
ls (List the current project with the tracked patch sets.)
rebase (Rebase the patch data.)
switch (Switch context to the specified project.)
term (Open a shell or run a command in the current project's directory.)
unlink (Delete all patch tracking data for the specified project.)
:patch
Signed-off-by: Robin Jarry <robin@jarry.cc>
Tested-by: Antonin Godard <antonin@godard.cc>
304 lines
6.8 KiB
Go
304 lines
6.8 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/log"
|
|
"git.sr.ht/~rjarry/aerc/lib/xdg"
|
|
"git.sr.ht/~rjarry/aerc/models"
|
|
"git.sr.ht/~rjarry/aerc/worker/types"
|
|
"git.sr.ht/~rjarry/go-opt/v2"
|
|
"git.sr.ht/~rockorager/vaxis"
|
|
)
|
|
|
|
// QuickTerm is an ephemeral terminal for running a single command and quitting.
|
|
func QuickTerm(args []string, stdin io.Reader, silent bool) (*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)
|
|
return
|
|
}
|
|
if silent {
|
|
app.RemoveTab(term, true)
|
|
} else {
|
|
app.PushStatus("Process complete, press any key to close.",
|
|
10*time.Second)
|
|
term.OnEvent = func(event vaxis.Event) bool {
|
|
app.RemoveTab(term, true)
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
term.OnStart = func() {
|
|
go func() {
|
|
defer log.PanicHandler()
|
|
defer pipe.Close()
|
|
|
|
if _, err := io.Copy(pipe, stdin); err != nil {
|
|
app.PushError(err.Error())
|
|
}
|
|
}()
|
|
}
|
|
|
|
return term, nil
|
|
}
|
|
|
|
// CompletePath provides filesystem completions given a starting path.
|
|
func CompletePath(path string, onlyDirs bool) []string {
|
|
return completePath(path, onlyDirs, app.SelectedAccountUiConfig().FuzzyComplete)
|
|
}
|
|
|
|
func completePath(path string, onlyDirs bool, fuzzyComplete bool) []string {
|
|
if path == ".." || strings.HasSuffix(path, "/..") {
|
|
return []string{path + "/"}
|
|
}
|
|
if path == "~" {
|
|
path = xdg.HomeDir() + "/"
|
|
} else if strings.HasPrefix(path, "~/") {
|
|
path = xdg.HomeDir() + strings.TrimPrefix(path, "~")
|
|
}
|
|
includeHidden := path == "."
|
|
if i := strings.LastIndex(path, "/"); i != -1 && i < len(path)-1 {
|
|
includeHidden = strings.HasPrefix(path[i+1:], ".")
|
|
}
|
|
|
|
const currentDir = "."
|
|
dir, search := filepath.Split(path)
|
|
// for `file` case dir will be `` which does not work in listDir
|
|
if dir == "" {
|
|
dir = currentDir
|
|
}
|
|
|
|
entries := listDir(dir, includeHidden)
|
|
filteredEntries := make([]string, 0, len(entries))
|
|
for _, m := range entries {
|
|
testM := m
|
|
if dir != currentDir {
|
|
testM = dir + m
|
|
}
|
|
if isDir(testM) {
|
|
m += "/"
|
|
} else if onlyDirs {
|
|
continue
|
|
}
|
|
filteredEntries = append(filteredEntries, m)
|
|
}
|
|
|
|
sort.Strings(filteredEntries)
|
|
results := filterList(
|
|
filteredEntries,
|
|
search,
|
|
func(s string) string {
|
|
if dir != currentDir {
|
|
s = dir + s
|
|
}
|
|
return opt.QuoteArg(xdg.TildeHome(s))
|
|
},
|
|
fuzzyComplete,
|
|
)
|
|
|
|
return results
|
|
}
|
|
|
|
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) ([]models.UID, error) {
|
|
// marked has priority over the selected message
|
|
marked, err := pm.MarkedMessages()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(marked) > 0 {
|
|
marked = expandFoldedThreads(pm, marked)
|
|
return marked, nil
|
|
}
|
|
msg, err := pm.SelectedMessage()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return expandFoldedThreads(pm, []models.UID{msg.Uid}), nil
|
|
}
|
|
|
|
func expandFoldedThreads(pm app.ProvidesMessages, uids []models.UID) []models.UID {
|
|
store := pm.Store()
|
|
if store == nil {
|
|
return uids
|
|
}
|
|
expanded := make([]models.UID, len(uids))
|
|
copy(expanded, uids)
|
|
for _, uid := range uids {
|
|
thread, err := store.Thread(uid)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if thread != nil && thread.FirstChild != nil && thread.FirstChild.Hidden > 0 {
|
|
_ = thread.Walk(func(t *types.Thread, _ int, __ error) error {
|
|
if t.Uid != uid {
|
|
expanded = append(expanded, t.Uid)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
}
|
|
if len(uids) != len(expanded) {
|
|
log.Debugf("expand folded threads: %v -> %v\n", uids, expanded)
|
|
}
|
|
return expanded
|
|
}
|
|
|
|
// UidsFromMessageInfos extracts a uid slice from a slice of MessageInfos
|
|
func UidsFromMessageInfos(msgs []*models.MessageInfo) []models.UID {
|
|
uids := make([]models.UID, len(msgs))
|
|
i := 0
|
|
for _, msg := range msgs {
|
|
uids[i] = msg.Uid
|
|
i++
|
|
}
|
|
return uids
|
|
}
|
|
|
|
func MsgInfoFromUids(store *lib.MessageStore, uids []models.UID, statusInfo func(string)) ([]*models.MessageInfo, error) {
|
|
infos := make([]*models.MessageInfo, len(uids))
|
|
needHeaders := make([]models.UID, 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 {
|
|
s, desc, found := strings.Cut(s, "\n")
|
|
s = opt.QuoteArg(s) + " "
|
|
if found {
|
|
s += "\n" + desc
|
|
}
|
|
return 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 {
|
|
return filterList(valid, search, postProc, app.SelectedAccountUiConfig().FuzzyComplete)
|
|
}
|
|
|
|
func filterList(
|
|
valid []string,
|
|
search string,
|
|
postProc func(string) string,
|
|
fuzzyComplete bool,
|
|
) []string {
|
|
if postProc == nil {
|
|
postProc = QuoteSpace
|
|
}
|
|
out := make([]string, 0, len(valid))
|
|
if fuzzyComplete {
|
|
nonexact := make([]string, 0, len(valid))
|
|
for _, e := range valid {
|
|
if strings.Contains(strings.ToLower(e), strings.ToLower(search)) {
|
|
out = append(out, postProc(e))
|
|
} else {
|
|
nonexact = append(nonexact, e)
|
|
}
|
|
}
|
|
matches := fuzzy.RankFindNormalizedFold(search, nonexact)
|
|
for _, v := range matches {
|
|
out = append(out, postProc(v.Target))
|
|
}
|
|
} else {
|
|
for _, v := range valid {
|
|
if hasCaseSmartPrefix(v, search) {
|
|
out = append(out, postProc(v))
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|