aboutsummaryrefslogblamecommitdiffstats
path: root/commands/util.go
blob: af8d3dfaecef8b8524f5b65c57e7dfeb986534aa (plain) (tree)
1
2
3
4
5
6
7
8
9


                
             
            
            
                 
                       
              
                 
              
 

                                                
                                    
                                    
                                        
                                    
                                       
                                             
                                  
                                     

 
                                                                                
                                                                       





                                                 
                                         





                                        
                                                  
                                                                           
                                                  
                        
                                                                                   
                                               
                                                                     
                                                         








                                             
                                                
 






                                                      
                                                  




                        

                                                                      
                                                        








                                                                          

         


                                                 

         
                                                  
 


                                   

                                    


                                                                               
                 
                                                                         

         
                             
 
                      



































                                                                         


                                                                               
                                                                  





                                                        
                                                        





                                        




























                                                                                              











                                                                         
 
                                                                                                                      
                                                       
                                        





                                                               
                                    
                                                              
                 
         














                                                                                                           

                         
 



                                    
                                                                      








                                                                           
                                            
                                                        
                                                                     
                                                             



                                                          
                                                              




                         
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, onlyDirs bool) []string {
	if path == ".." || strings.HasSuffix(path, "/..") {
		return []string{path + "/"}
	}
	if path == "~" || 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:], ".")
	}

	matches, err := filepath.Glob(path + "*")
	if err != nil || matches == nil {
		return nil
	}

	results := make([]string, 0, len(matches))

	for _, m := range matches {
		if isDir(m) {
			m += "/"
		} else if onlyDirs {
			continue
		}
		if strings.HasPrefix(filepath.Base(m), ".") && !includeHidden {
			continue
		}
		results = append(results, opt.QuoteArg(xdg.TildeHome(m)))
	}

	sort.Strings(results)

	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) ([]uint32, 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, []uint32{msg.Uid}), nil
}

func expandFoldedThreads(pm app.ProvidesMessages, uids []uint32) []uint32 {
	store := pm.Store()
	if store == nil {
		return uids
	}
	expanded := make([]uint32, 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) []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
}