commands: use completion from go-opt

Implement command completion with complete struct field tags from the
get-opt library introduced earlier.

Changelog-changed: Improved command completion.
Signed-off-by: Robin Jarry <robin@jarry.cc>
Reviewed-by: Koni Marti <koni.marti@gmail.com>
Tested-by: Moritz Poldrack <moritz@poldrack.dev>
Tested-by: Inwit <inwit@sindominio.net>
This commit is contained in:
Robin Jarry
2023-10-22 23:23:18 +02:00
parent 3085165659
commit abe228b14d
81 changed files with 197 additions and 968 deletions

View File

@@ -11,7 +11,7 @@ import (
var history map[string]string
type ChangeFolder struct {
Folder string `opt:"..." metavar:"<folder>"`
Folder string `opt:"folder" complete:"CompleteFolder"`
}
func init() {
@@ -23,8 +23,8 @@ func (ChangeFolder) Aliases() []string {
return []string{"cf"}
}
func (ChangeFolder) Complete(args []string) []string {
return commands.GetFolders(args)
func (*ChangeFolder) CompleteFolder(arg string) []string {
return commands.GetFolders(arg)
}
func (c ChangeFolder) Execute(args []string) error {

View File

@@ -16,10 +16,6 @@ func (CheckMail) Aliases() []string {
return []string{"check-mail"}
}
func (CheckMail) Complete(args []string) []string {
return nil
}
func (CheckMail) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {

View File

@@ -19,10 +19,6 @@ func (Clear) Aliases() []string {
return []string{"clear"}
}
func (Clear) Complete(args []string) []string {
return nil
}
func (c Clear) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {

View File

@@ -11,12 +11,13 @@ import (
"github.com/emersion/go-message/mail"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/config"
)
type Compose struct {
Headers string `opt:"-H" action:"ParseHeader"`
Template string `opt:"-T"`
Template string `opt:"-T" complete:"CompleteTemplate"`
Edit bool `opt:"-e"`
NoEdit bool `opt:"-E"`
Body string `opt:"..." required:"false"`
@@ -37,12 +38,12 @@ func (c *Compose) ParseHeader(arg string) error {
return nil
}
func (Compose) Aliases() []string {
return []string{"compose"}
func (*Compose) CompleteTemplate(arg string) []string {
return commands.GetTemplates(arg)
}
func (Compose) Complete(args []string) []string {
return nil
func (Compose) Aliases() []string {
return []string{"compose"}
}
func (c Compose) Execute(args []string) error {

View File

@@ -18,10 +18,6 @@ func (Connection) Aliases() []string {
return []string{"connect", "disconnect"}
}
func (Connection) Complete(args []string) []string {
return nil
}
func (c Connection) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {

View File

@@ -16,10 +16,6 @@ func (ExpandCollapseFolder) Aliases() []string {
return []string{"expand-folder", "collapse-folder"}
}
func (ExpandCollapseFolder) Complete(args []string) []string {
return nil
}
func (ExpandCollapseFolder) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {

View File

@@ -17,7 +17,7 @@ import (
)
type ExportMbox struct {
Filename string `opt:"filename"`
Filename string `opt:"filename" complete:"CompleteFilename"`
}
func init() {
@@ -28,8 +28,8 @@ func (ExportMbox) Aliases() []string {
return []string{"export-mbox"}
}
func (ExportMbox) Complete(args []string) []string {
return commands.CompletePath(filepath.Join(args...))
func (*ExportMbox) CompleteFilename(arg string) []string {
return commands.CompletePath(arg)
}
func (e ExportMbox) Execute(args []string) error {

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"sync/atomic"
"time"
@@ -19,7 +18,7 @@ import (
)
type ImportMbox struct {
Filename string `opt:"filename"`
Filename string `opt:"filename" complete:"CompleteFilename"`
}
func init() {
@@ -30,8 +29,8 @@ func (ImportMbox) Aliases() []string {
return []string{"import-mbox"}
}
func (ImportMbox) Complete(args []string) []string {
return commands.CompletePath(filepath.Join(args...))
func (*ImportMbox) CompleteFilename(arg string) []string {
return commands.CompletePath(arg)
}
func (i ImportMbox) Execute(args []string) error {

View File

@@ -2,15 +2,15 @@ package account
import (
"errors"
"strings"
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/worker/types"
)
type MakeDir struct {
Folder string `opt:"..." metavar:"<folder>"`
Folder string `opt:"folder" complete:"CompleteFolder"`
}
func init() {
@@ -21,26 +21,15 @@ func (MakeDir) Aliases() []string {
return []string{"mkdir"}
}
func (MakeDir) Complete(args []string) []string {
if len(args) == 0 {
func (*MakeDir) CompleteFolder(arg string) []string {
acct := app.SelectedAccount()
if acct == nil {
return nil
}
name := strings.Join(args, " ")
list := app.SelectedAccount().Directories().List()
inboxes := make([]string, len(list))
copy(inboxes, list)
// remove inboxes that don't match and append the path separator to all
// others
for i := len(inboxes) - 1; i >= 0; i-- {
if !strings.HasPrefix(inboxes[i], name) && name != "" {
inboxes = append(inboxes[:i], inboxes[i+1:]...)
continue
}
inboxes[i] += app.SelectedAccount().Worker().PathSeparator()
}
return inboxes
return commands.FilterList(
acct.Directories().List(), arg, "",
app.SelectedAccount().Worker().PathSeparator(),
app.SelectedAccountUiConfig().FuzzyComplete)
}
func (m MakeDir) Execute(args []string) error {

View File

@@ -18,10 +18,6 @@ func (NextPrevFolder) Aliases() []string {
return []string{"next-folder", "prev-folder"}
}
func (NextPrevFolder) Complete(args []string) []string {
return nil
}
func (np NextPrevFolder) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {

View File

@@ -17,10 +17,6 @@ func (NextPrevResult) Aliases() []string {
return []string{"next-result", "prev-result"}
}
func (NextPrevResult) Complete(args []string) []string {
return nil
}
func (NextPrevResult) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {

View File

@@ -35,10 +35,6 @@ func (NextPrevMsg) Aliases() []string {
return []string{"next", "next-message", "prev", "prev-message"}
}
func (NextPrevMsg) Complete(args []string) []string {
return nil
}
func (np NextPrevMsg) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {

View File

@@ -16,7 +16,7 @@ type Recover struct {
Force bool `opt:"-f"`
Edit bool `opt:"-e"`
NoEdit bool `opt:"-E"`
File string `opt:"file"`
File string `opt:"file" complete:"CompleteFile"`
}
func init() {
@@ -31,7 +31,7 @@ func (Recover) Options() string {
return "feE"
}
func (r Recover) Complete(args []string) []string {
func (*Recover) CompleteFile(arg string) []string {
// file name of temp file is hard-coded in the NewComposer() function
files, err := filepath.Glob(
filepath.Join(os.TempDir(), "aerc-compose-*.eml"),
@@ -39,8 +39,7 @@ func (r Recover) Complete(args []string) []string {
if err != nil {
return nil
}
return commands.CompletionFromList(files,
commands.Operands(args, r.Options()))
return commands.CompletionFromList(files, arg)
}
func (r Recover) Execute(args []string) error {

View File

@@ -20,10 +20,6 @@ func (RemoveDir) Aliases() []string {
return []string{"rmdir"}
}
func (RemoveDir) Complete(args []string) []string {
return nil
}
func (r RemoveDir) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {

View File

@@ -23,12 +23,12 @@ type SearchFilter struct {
Body bool `opt:"-b"`
All bool `opt:"-a"`
Headers textproto.MIMEHeader `opt:"-H" action:"ParseHeader" metavar:"<header>:<value>"`
WithFlags models.Flags `opt:"-x" action:"ParseFlag"`
WithoutFlags models.Flags `opt:"-X" action:"ParseNotFlag"`
To []string `opt:"-t" action:"ParseTo"`
From []string `opt:"-f" action:"ParseFrom"`
Cc []string `opt:"-c" action:"ParseCc"`
StartDate time.Time `opt:"-d" action:"ParseDate"`
WithFlags models.Flags `opt:"-x" action:"ParseFlag" complete:"CompleteFlag"`
WithoutFlags models.Flags `opt:"-X" action:"ParseNotFlag" complete:"CompleteFlag"`
To []string `opt:"-t" action:"ParseTo" complete:"CompleteAddress"`
From []string `opt:"-f" action:"ParseFrom" complete:"CompleteAddress"`
Cc []string `opt:"-c" action:"ParseCc" complete:"CompleteAddress"`
StartDate time.Time `opt:"-d" action:"ParseDate" complete:"CompleteDate"`
EndDate time.Time
Terms string `opt:"..." required:"false"`
}
@@ -37,33 +37,20 @@ func init() {
register(SearchFilter{})
}
func (SearchFilter) Options() string {
return "rubax:X:t:H:f:c:d:"
}
func (SearchFilter) Aliases() []string {
return []string{"search", "filter"}
}
func (s SearchFilter) CompleteOption(
r rune,
search string,
) []string {
var valid []string
switch r {
case 'x', 'X':
valid = commands.GetFlagList()
case 't', 'f', 'c':
valid = commands.GetAddress(search)
case 'd':
valid = commands.GetDateList()
default:
}
return commands.CompletionFromList(valid, []string{search})
func (*SearchFilter) CompleteFlag(arg string) []string {
return commands.CompletionFromList(commands.GetFlagList(), arg)
}
func (SearchFilter) Complete(args []string) []string {
return nil
func (*SearchFilter) CompleteAddress(arg string) []string {
return commands.CompletionFromList(commands.GetAddress(arg), arg)
}
func (*SearchFilter) CompleteDate(arg string) []string {
return commands.CompletionFromList(commands.GetDateList(), arg)
}
func (s *SearchFilter) ParseRead(arg string) error {

View File

@@ -18,10 +18,6 @@ func (SelectMessage) Aliases() []string {
return []string{"select", "select-message"}
}
func (SelectMessage) Complete(args []string) []string {
return nil
}
func (s SelectMessage) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {

View File

@@ -2,7 +2,6 @@ package account
import (
"errors"
"strings"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
@@ -13,6 +12,9 @@ import (
type Sort struct {
Unused struct{} `opt:"-"`
// these fields are only used for completion
Reverse bool `opt:"-r"`
Criteria []string `opt:"criteria" complete:"CompleteCriteria"`
}
func init() {
@@ -23,46 +25,20 @@ func (Sort) Aliases() []string {
return []string{"sort"}
}
func (Sort) Complete(args []string) []string {
supportedCriteria := []string{
"arrival",
"cc",
"date",
"from",
"read",
"size",
"subject",
"to",
"flagged",
}
if len(args) == 0 {
return supportedCriteria
}
last := args[len(args)-1]
var completions []string
currentPrefix := strings.Join(args, " ") + " "
// if there is a completed criteria or option then suggest all again
for _, criteria := range append(supportedCriteria, "-r") {
if criteria == last {
for _, criteria := range supportedCriteria {
completions = append(completions, currentPrefix+criteria)
}
return completions
}
}
var supportedCriteria = []string{
"arrival",
"cc",
"date",
"from",
"read",
"size",
"subject",
"to",
"flagged",
}
currentPrefix = strings.Join(args[:len(args)-1], " ")
if len(args) > 1 {
currentPrefix += " "
}
// last was beginning an option
if last == "-" {
return []string{currentPrefix + "-r"}
}
// the last item is not complete
completions = commands.FilterList(supportedCriteria, last, currentPrefix,
app.SelectedAccountUiConfig().FuzzyComplete)
return completions
func (*Sort) CompleteCriteria(arg string) []string {
return commands.CompletionFromList(supportedCriteria, arg)
}
func (Sort) Execute(args []string) error {

View File

@@ -33,10 +33,6 @@ func (Split) Aliases() []string {
return []string{"split", "vsplit", "hsplit"}
}
func (Split) Complete(args []string) []string {
return nil
}
func (s Split) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {

View File

@@ -19,10 +19,6 @@ func (ViewMessage) Aliases() []string {
return []string{"view-message", "view"}
}
func (ViewMessage) Complete(args []string) []string {
return nil
}
func (v ViewMessage) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {

View File

@@ -12,7 +12,7 @@ import (
var previousDir string
type ChangeDirectory struct {
Target string `opt:"directory" default:"~"`
Target string `opt:"directory" default:"~" complete:"CompleteTarget"`
}
func init() {
@@ -23,9 +23,8 @@ func (ChangeDirectory) Aliases() []string {
return []string{"cd"}
}
func (ChangeDirectory) Complete(args []string) []string {
path := strings.Join(args, " ")
completions := CompletePath(path)
func (*ChangeDirectory) CompleteTarget(arg string) []string {
completions := CompletePath(arg)
var dirs []string
for _, c := range completions {

View File

@@ -18,10 +18,6 @@ func (Choose) Aliases() []string {
return []string{"choose"}
}
func (Choose) Complete(args []string) []string {
return nil
}
func (Choose) Execute(args []string) error {
if len(args) < 5 || len(args)%4 != 1 {
return chooseUsage(args[0])

View File

@@ -3,12 +3,13 @@ package commands
import (
"bytes"
"errors"
"path"
"reflect"
"sort"
"strings"
"unicode"
"git.sr.ht/~rjarry/go-opt"
"github.com/google/shlex"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/config"
@@ -21,17 +22,6 @@ import (
type Command interface {
Aliases() []string
Execute([]string) error
Complete([]string) []string
}
type OptionsProvider interface {
Command
Options() string
}
type OptionCompleter interface {
OptionsProvider
CompleteOption(rune, string) []string
}
type Commands map[string]Command
@@ -174,6 +164,7 @@ func GetTemplateCompletion(
templates.Terms(),
strings.TrimSpace(search),
"",
"",
app.SelectedAccountUiConfig().FuzzyComplete,
)
return options, prefix + padding, true
@@ -194,117 +185,62 @@ func GetTemplateCompletion(
func GetCompletions(
cmd Command, args *opt.Args,
) (options []string, prefix string) {
// complete options
var spec string
if provider, ok := cmd.(OptionsProvider); ok {
spec = provider.Options()
}
parser, err := newParser(args.String(), spec, strings.HasSuffix(args.String(), " "))
if err != nil {
log.Debugf("completion parser failed: %v", err)
return
}
switch parser.kind {
case SHORT_OPTION:
for _, r := range strings.ReplaceAll(spec, ":", "") {
if strings.ContainsRune(parser.flag, r) {
continue
}
option := string(r)
if strings.Contains(spec, option+":") {
option += " "
}
options = append(options, option)
}
prefix = args.String()
case OPTION_ARGUMENT:
cmpl, ok := cmd.(OptionCompleter)
if !ok {
return
}
stem := args.String()
if parser.arg != "" {
stem = strings.TrimSuffix(stem, parser.arg)
}
pad := ""
if !strings.HasSuffix(stem, " ") {
pad += " "
}
s := parser.flag
r := rune(s[len(s)-1])
for _, option := range cmpl.CompleteOption(r, parser.arg) {
options = append(options, pad+escape(option)+" ")
}
prefix = stem
case OPERAND:
clone := args.Clone()
clone.Cut(clone.Count() - parser.optind)
args.Shift(1)
for _, option := range cmd.Complete(args.Args()) {
if strings.Contains(option, " ") {
option = escape(option)
}
options = append(options, " "+option)
}
prefix = clone.String()
}
return
// copy zeroed struct
tmp := reflect.New(reflect.TypeOf(cmd)).Interface().(Command)
spec := opt.NewCmdSpec(args.Arg(0), tmp)
return spec.GetCompletions(args)
}
func GetFolders(args []string) []string {
func GetFolders(arg string) []string {
acct := app.SelectedAccount()
if acct == nil {
return make([]string, 0)
}
if len(args) == 0 {
return acct.Directories().List()
}
return FilterList(acct.Directories().List(), args[0], "", acct.UiConfig().FuzzyComplete)
return CompletionFromList(acct.Directories().List(), arg)
}
// CompletionFromList provides a convenience wrapper for commands to use in the
// Complete function. It simply matches the items provided in valid
func CompletionFromList(valid []string, args []string) []string {
if len(args) == 0 {
return valid
func GetTemplates(arg string) []string {
templates := make(map[string]bool)
for _, dir := range config.Templates.TemplateDirs {
for _, f := range listDir(dir, false) {
if !isDir(path.Join(dir, f)) {
templates[f] = true
}
}
}
return FilterList(valid, args[0], "", app.SelectedAccountUiConfig().FuzzyComplete)
names := make([]string, len(templates))
for n := range templates {
names = append(names, n)
}
sort.Strings(names)
return CompletionFromList(names, arg)
}
func GetLabels(args []string) []string {
// CompletionFromList provides a convenience wrapper for commands to use in a
// complete callback. It simply matches the items provided in valid
func CompletionFromList(valid []string, arg string) []string {
return FilterList(valid, arg, "", "", app.SelectedAccountUiConfig().FuzzyComplete)
}
func GetLabels(arg string) []string {
acct := app.SelectedAccount()
if acct == nil {
return make([]string, 0)
}
if len(args) == 0 {
return acct.Labels()
}
// + and - are used to denote tag addition / removal and need to be striped
// only the last tag should be completed, so that multiple labels can be
// selected
last := args[len(args)-1]
others := strings.Join(args[:len(args)-1], " ")
var prefix string
switch last[0] {
case '+':
prefix = "+"
case '-':
prefix = "-"
default:
prefix = ""
if arg != "" {
// + and - are used to denote tag addition / removal and need to
// be striped only the last tag should be completed, so that
// multiple labels can be selected
switch arg[0] {
case '+':
prefix = "+"
case '-':
prefix = "-"
}
arg = strings.TrimLeft(arg, "+-")
}
trimmed := strings.TrimLeft(last, "+-")
var prev string
if len(others) > 0 {
prev = others + " "
}
out := FilterList(acct.Labels(), trimmed, prev+prefix, acct.UiConfig().FuzzyComplete)
return out
return FilterList(acct.Labels(), arg, prefix, " ", acct.UiConfig().FuzzyComplete)
}
// hasCaseSmartPrefix checks whether s starts with prefix, using a case
@@ -324,19 +260,3 @@ func hasUpper(s string) bool {
}
return false
}
// splitCmd splits the command into arguments
func splitCmd(cmd string) ([]string, error) {
args, err := shlex.Split(cmd)
if err != nil {
return nil, err
}
return args, nil
}
func escape(s string) string {
if strings.Contains(s, " ") {
return strings.ReplaceAll(s, " ", "\\ ")
}
return s
}

View File

@@ -14,10 +14,6 @@ func (Abort) Aliases() []string {
return []string{"abort"}
}
func (Abort) Complete(args []string) []string {
return nil
}
func (Abort) Execute(args []string) error {
composer, _ := app.SelectedTabContent().(*app.Composer)
app.RemoveTab(composer, true)

View File

@@ -14,10 +14,6 @@ func (AttachKey) Aliases() []string {
return []string{"attach-key"}
}
func (AttachKey) Complete(args []string) []string {
return nil
}
func (AttachKey) Execute(args []string) error {
composer, _ := app.SelectedTabContent().(*app.Composer)
return composer.SetAttachKey(!composer.AttachKey())

View File

@@ -23,7 +23,8 @@ import (
type Attach struct {
Menu bool `opt:"-m"`
Name string `opt:"-r"`
Path string `opt:"..." metavar:"<path>" required:"false"`
Path string `opt:"path" required:"false" complete:"CompletePath"`
Args string `opt:"..." required:"false"`
}
func init() {
@@ -34,9 +35,8 @@ func (Attach) Aliases() []string {
return []string{"attach"}
}
func (Attach) Complete(args []string) []string {
path := strings.Join(args, " ")
return commands.CompletePath(path)
func (*Attach) CompletePath(arg string) []string {
return commands.CompletePath(arg)
}
func (a Attach) Execute(args []string) error {
@@ -52,6 +52,9 @@ func (a Attach) Execute(args []string) error {
}
return a.readCommand()
default:
if a.Args != "" {
return errors.New("only a single path is supported")
}
return a.addPath(a.Path)
}
}
@@ -186,7 +189,7 @@ func (a Attach) openMenu() error {
}
func (a Attach) readCommand() error {
cmd := exec.Command("sh", "-c", a.Path)
cmd := exec.Command("sh", "-c", a.Path+" "+a.Args)
data, err := cmd.Output()
if err != nil {

View File

@@ -16,10 +16,6 @@ func (CC) Aliases() []string {
return []string{"cc", "bcc"}
}
func (CC) Complete(args []string) []string {
return nil
}
func (c CC) Execute(args []string) error {
composer, _ := app.SelectedTabContent().(*app.Composer)

View File

@@ -4,10 +4,11 @@ import (
"fmt"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
)
type Detach struct {
Path string `opt:"path" required:"false"`
Path string `opt:"path" required:"false" complete:"CompletePath"`
}
func init() {
@@ -18,9 +19,9 @@ func (Detach) Aliases() []string {
return []string{"detach"}
}
func (Detach) Complete(args []string) []string {
func (*Detach) CompletePath(arg string) []string {
composer, _ := app.SelectedTabContent().(*app.Composer)
return composer.GetAttachments()
return commands.CompletionFromList(composer.GetAttachments(), arg)
}
func (d Detach) Execute(args []string) error {

View File

@@ -20,10 +20,6 @@ func (Edit) Aliases() []string {
return []string{"edit"}
}
func (Edit) Complete(args []string) []string {
return nil
}
func (e Edit) Execute(args []string) error {
composer, ok := app.SelectedTabContent().(*app.Composer)
if !ok {

View File

@@ -14,10 +14,6 @@ func (Encrypt) Aliases() []string {
return []string{"encrypt"}
}
func (Encrypt) Complete(args []string) []string {
return nil
}
func (Encrypt) Execute(args []string) error {
composer, _ := app.SelectedTabContent().(*app.Composer)
composer.SetEncrypt(!composer.Encrypt())

View File

@@ -11,7 +11,7 @@ import (
type Header struct {
Force bool `opt:"-f"`
Remove bool `opt:"-d"`
Name string `opt:"name"`
Name string `opt:"name" complete:"CompleteHeaders"`
Value string `opt:"..." required:"false"`
}
@@ -37,8 +37,8 @@ func (Header) Options() string {
return "fd"
}
func (Header) Complete(args []string) []string {
return commands.CompletionFromList(headers, args)
func (*Header) CompleteHeaders(arg string) []string {
return commands.CompletionFromList(headers, arg)
}
func (h Header) Execute(args []string) error {

View File

@@ -11,7 +11,7 @@ import (
type Multipart struct {
Remove bool `opt:"-d"`
Mime string `opt:"mime" metavar:"<mime/type>"`
Mime string `opt:"mime" metavar:"<mime/type>" complete:"CompleteMime"`
}
func init() {
@@ -22,13 +22,12 @@ func (Multipart) Aliases() []string {
return []string{"multipart"}
}
func (Multipart) Complete(args []string) []string {
func (*Multipart) CompleteMime(arg string) []string {
var completions []string
completions = append(completions, "-d")
for mime := range config.Converters {
completions = append(completions, mime)
}
return commands.CompletionFromList(completions, args)
return commands.CompletionFromList(completions, arg)
}
func (m Multipart) Execute(args []string) error {

View File

@@ -14,10 +14,6 @@ func (NextPrevField) Aliases() []string {
return []string{"next-field", "prev-field"}
}
func (NextPrevField) Complete(args []string) []string {
return nil
}
func (NextPrevField) Execute(args []string) error {
composer, _ := app.SelectedTabContent().(*app.Composer)
if args[0] == "prev-field" {

View File

@@ -14,7 +14,7 @@ import (
)
type Postpone struct {
Folder string `opt:"-t"`
Folder string `opt:"-t" complete:"CompleteFolder"`
}
func init() {
@@ -25,20 +25,8 @@ func (Postpone) Aliases() []string {
return []string{"postpone"}
}
func (Postpone) Options() string {
return "t:"
}
func (Postpone) CompleteOption(r rune, arg string) []string {
var valid []string
if r == 't' {
valid = commands.GetFolders([]string{arg})
}
return commands.CompletionFromList(valid, []string{arg})
}
func (Postpone) Complete(args []string) []string {
return nil
func (*Postpone) CompleteFolder(arg string) []string {
return commands.GetFolders(arg)
}
func (p Postpone) Execute(args []string) error {

View File

@@ -28,8 +28,8 @@ import (
)
type Send struct {
Archive string `opt:"-a" action:"ParseArchive" metavar:"flat|year|month"`
CopyTo string `opt:"-t"`
Archive string `opt:"-a" action:"ParseArchive" metavar:"flat|year|month" complete:"CompleteArchive"`
CopyTo string `opt:"-t" complete:"CompleteFolders"`
}
func init() {
@@ -40,19 +40,12 @@ func (Send) Aliases() []string {
return []string{"send"}
}
func (Send) Options() string {
return "a:t:"
func (*Send) CompleteArchive(arg string) []string {
return commands.CompletionFromList(msg.ARCHIVE_TYPES, arg)
}
func (s Send) CompleteOption(r rune, term string) []string {
if r == 't' {
return commands.GetFolders([]string{term})
}
return nil
}
func (Send) Complete(args []string) []string {
return nil
func (*Send) CompleteFolders(arg string) []string {
return commands.GetFolders(arg)
}
func (s *Send) ParseArchive(arg string) error {

View File

@@ -16,10 +16,6 @@ func (Sign) Aliases() []string {
return []string{"sign"}
}
func (Sign) Complete(args []string) []string {
return nil
}
func (Sign) Execute(args []string) error {
composer, _ := app.SelectedTabContent().(*app.Composer)

View File

@@ -4,6 +4,7 @@ import (
"errors"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
)
type AccountSwitcher interface {
@@ -13,7 +14,7 @@ type AccountSwitcher interface {
type SwitchAccount struct {
Next bool `opt:"-n"`
Prev bool `opt:"-p"`
Account string `opt:"..." metavar:"<account>" required:"false"`
Account string `opt:"account" required:"false" complete:"CompleteAccount"`
}
func init() {
@@ -24,8 +25,8 @@ func (SwitchAccount) Aliases() []string {
return []string{"switch-account"}
}
func (SwitchAccount) Complete(args []string) []string {
return app.AccountNames()
func (*SwitchAccount) CompleteAccount(arg string) []string {
return commands.CompletionFromList(app.AccountNames(), arg)
}
func (s SwitchAccount) Execute(args []string) error {

View File

@@ -9,7 +9,7 @@ import (
)
type ChangeTab struct {
Tab string `opt:"tab"`
Tab string `opt:"tab" complete:"CompleteTab"`
}
func init() {
@@ -20,12 +20,8 @@ func (ChangeTab) Aliases() []string {
return []string{"ct", "change-tab"}
}
func (ChangeTab) Complete(args []string) []string {
if len(args) == 0 {
return app.TabNames()
}
joinedArgs := strings.Join(args, " ")
return FilterList(app.TabNames(), joinedArgs, "", app.SelectedAccountUiConfig().FuzzyComplete)
func (*ChangeTab) CompleteTab(arg string) []string {
return CompletionFromList(app.TabNames(), arg)
}
func (c ChangeTab) Execute(args []string) error {

View File

@@ -5,14 +5,13 @@ import (
"fmt"
"io"
"os"
"strings"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/lib"
)
type Eml struct {
Path string `opt:"path" required:"false"`
Path string `opt:"path" required:"false" complete:"CompletePath"`
}
func init() {
@@ -23,8 +22,8 @@ func (Eml) Aliases() []string {
return []string{"eml", "preview"}
}
func (Eml) Complete(args []string) []string {
return CompletePath(strings.Join(args, " "))
func (*Eml) CompletePath(arg string) []string {
return CompletePath(arg)
}
func (e Eml) Execute(args []string) error {

View File

@@ -22,10 +22,6 @@ func (ExecCmd) Aliases() []string {
return []string{"exec"}
}
func (ExecCmd) Complete(args []string) []string {
return nil
}
func (e ExecCmd) Execute(args []string) error {
cmd := exec.Command(e.Args[0], e.Args[1:]...)
env := os.Environ()

View File

@@ -7,7 +7,7 @@ import (
)
type Help struct {
Topic string `opt:"topic" action:"ParseTopic" default:"aerc"`
Topic string `opt:"topic" action:"ParseTopic" default:"aerc" complete:"CompleteTopic"`
}
var pages = []string{
@@ -35,8 +35,8 @@ func (Help) Aliases() []string {
return []string{"help"}
}
func (Help) Complete(args []string) []string {
return CompletionFromList(pages, args)
func (*Help) CompleteTopic(arg string) []string {
return CompletionFromList(pages, arg)
}
func (h *Help) ParseTopic(arg string) error {

View File

@@ -32,10 +32,6 @@ func (MoveTab) Aliases() []string {
return []string{"move-tab"}
}
func (MoveTab) Complete(args []string) []string {
return nil
}
func (m MoveTab) Execute(args []string) error {
app.MoveTab(m.Index, m.Relative)
return nil

View File

@@ -21,7 +21,7 @@ const (
var ARCHIVE_TYPES = []string{ARCHIVE_FLAT, ARCHIVE_YEAR, ARCHIVE_MONTH}
type Archive struct {
Type string `opt:"type" action:"ParseArchiveType" metavar:"flat|year|month"`
Type string `opt:"type" action:"ParseArchiveType" metavar:"flat|year|month" complete:"CompleteType"`
}
func (a *Archive) ParseArchiveType(arg string) error {
@@ -42,9 +42,8 @@ func (Archive) Aliases() []string {
return []string{"archive"}
}
func (Archive) Complete(args []string) []string {
valid := []string{"flat", "year", "month"}
return commands.CompletionFromList(valid, args)
func (*Archive) CompleteType(arg string) []string {
return commands.CompletionFromList(ARCHIVE_TYPES, arg)
}
func (a Archive) Execute(args []string) error {

View File

@@ -10,7 +10,7 @@ import (
type Copy struct {
CreateFolders bool `opt:"-p"`
Folder string `opt:"..." metavar:"<folder>"`
Folder string `opt:"folder" complete:"CompleteFolder"`
}
func init() {
@@ -21,8 +21,8 @@ func (Copy) Aliases() []string {
return []string{"cp", "copy"}
}
func (Copy) Complete(args []string) []string {
return commands.GetFolders(args)
func (*Copy) CompleteFolder(arg string) []string {
return commands.GetFolders(arg)
}
func (c Copy) Execute(args []string) error {

View File

@@ -21,10 +21,6 @@ func (Delete) Aliases() []string {
return []string{"delete", "delete-message"}
}
func (Delete) Complete(args []string) []string {
return nil
}
func (Delete) Execute(args []string) error {
h := newHelper()
store, err := h.store()

View File

@@ -25,10 +25,6 @@ func (Envelope) Aliases() []string {
return []string{"envelope"}
}
func (Envelope) Complete(args []string) []string {
return nil
}
func (e Envelope) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {

View File

@@ -17,10 +17,6 @@ func (Fold) Aliases() []string {
return []string{"fold", "unfold"}
}
func (Fold) Complete(args []string) []string {
return nil
}
func (Fold) Execute(args []string) error {
h := newHelper()
store, err := h.store()

View File

@@ -13,6 +13,7 @@ import (
"sync"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/format"
@@ -27,7 +28,7 @@ type forward struct {
AttachFull bool `opt:"-F"`
Edit bool `opt:"-e"`
NoEdit bool `opt:"-E"`
Template string `opt:"-T"`
Template string `opt:"-T" complete:"CompleteTemplate"`
To []string `opt:"..." required:"false"`
}
@@ -39,8 +40,8 @@ func (forward) Aliases() []string {
return []string{"forward"}
}
func (forward) Complete(args []string) []string {
return nil
func (*forward) CompleteTemplate(arg string) []string {
return commands.GetTemplates(arg)
}
func (f forward) Execute(args []string) error {

View File

@@ -28,10 +28,6 @@ func (invite) Aliases() []string {
return []string{"accept", "accept-tentative", "decline"}
}
func (invite) Complete(args []string) []string {
return nil
}
func (i invite) Execute(args []string) error {
acct := app.SelectedAccount()
if acct == nil {

View File

@@ -20,10 +20,6 @@ func (Mark) Aliases() []string {
return []string{"mark", "unmark", "remark"}
}
func (Mark) Complete(args []string) []string {
return nil
}
func (m Mark) Execute(args []string) error {
h := newHelper()
OnSelectedMessage := func(fn func(uint32)) error {

View File

@@ -9,7 +9,7 @@ import (
)
type ModifyLabels struct {
Labels []string `opt:"..." metavar:"[+-]<label>"`
Labels []string `opt:"..." metavar:"[+-]<label>" complete:"CompleteLabels"`
}
func init() {
@@ -20,8 +20,8 @@ func (ModifyLabels) Aliases() []string {
return []string{"modify-labels", "tag"}
}
func (ModifyLabels) Complete(args []string) []string {
return commands.GetLabels(args)
func (*ModifyLabels) CompleteLabels(arg string) []string {
return commands.GetLabels(arg)
}
func (m ModifyLabels) Execute(args []string) error {

View File

@@ -14,7 +14,7 @@ import (
type Move struct {
CreateFolders bool `opt:"-p"`
Folder string `opt:"..." metavar:"<folder>"`
Folder string `opt:"folder" complete:"CompleteFolder"`
}
func init() {
@@ -25,8 +25,8 @@ func (Move) Aliases() []string {
return []string{"mv", "move"}
}
func (Move) Complete(args []string) []string {
return commands.GetFolders(args)
func (*Move) CompleteFolder(arg string) []string {
return commands.GetFolders(arg)
}
func (m Move) Execute(args []string) error {

View File

@@ -32,10 +32,6 @@ func (Pipe) Aliases() []string {
return []string{"pipe"}
}
func (Pipe) Complete(args []string) []string {
return nil
}
func (p Pipe) Execute(args []string) error {
if p.Full && p.Part {
return errors.New("-m and -p are mutually exclusive")

View File

@@ -6,6 +6,7 @@ import (
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
)
@@ -13,7 +14,7 @@ import (
type FlagMsg struct {
Toggle bool `opt:"-t"`
Answered bool `opt:"-a" aliases:"flag,unflag"`
Flag models.Flags `opt:"-x" aliases:"flag,unflag" action:"ParseFlag"`
Flag models.Flags `opt:"-x" aliases:"flag,unflag" action:"ParseFlag" complete:"CompleteFlag"`
FlagName string
}
@@ -25,10 +26,6 @@ func (FlagMsg) Aliases() []string {
return []string{"flag", "unflag", "read", "unread"}
}
func (FlagMsg) Complete(args []string) []string {
return nil
}
func (f *FlagMsg) ParseFlag(arg string) error {
switch strings.ToLower(arg) {
case "seen":
@@ -46,6 +43,12 @@ func (f *FlagMsg) ParseFlag(arg string) error {
return nil
}
var validFlags = []string{"seen", "answered", "flagged"}
func (*FlagMsg) CompleteFlag(arg string) []string {
return commands.CompletionFromList(validFlags, arg)
}
// If this was called as 'flag' or 'unflag', without the toggle (-t)
// option, then it will flag the corresponding messages with the given
// flag. If the toggle option was given, it will individually toggle

View File

@@ -31,10 +31,6 @@ func (Recall) Aliases() []string {
return []string{"recall"}
}
func (Recall) Complete(args []string) []string {
return nil
}
func (r Recall) Execute(args []string) error {
editHeaders := (config.Compose.EditHeaders || r.Edit) && !r.NoEdit

View File

@@ -10,6 +10,7 @@ import (
"time"
"git.sr.ht/~rjarry/aerc/app"
"git.sr.ht/~rjarry/aerc/commands"
"git.sr.ht/~rjarry/aerc/commands/account"
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib"
@@ -26,7 +27,7 @@ type reply struct {
All bool `opt:"-a"`
Close bool `opt:"-c"`
Quote bool `opt:"-q"`
Template string `opt:"-T"`
Template string `opt:"-T" complete:"CompleteTemplate"`
Edit bool `opt:"-e"`
NoEdit bool `opt:"-E"`
}
@@ -39,8 +40,8 @@ func (reply) Aliases() []string {
return []string{"reply"}
}
func (reply) Complete(args []string) []string {
return nil
func (*reply) CompleteTemplate(arg string) []string {
return commands.GetTemplates(arg)
}
func (r reply) Execute(args []string) error {

View File

@@ -14,10 +14,6 @@ func (ToggleThreadContext) Aliases() []string {
return []string{"toggle-thread-context"}
}
func (ToggleThreadContext) Complete(args []string) []string {
return nil
}
func (ToggleThreadContext) Execute(args []string) error {
h := newHelper()
store, err := h.store()

View File

@@ -15,10 +15,6 @@ func (ToggleThreads) Aliases() []string {
return []string{"toggle-threads"}
}
func (ToggleThreads) Complete(args []string) []string {
return nil
}
func (ToggleThreads) Execute(args []string) error {
h := newHelper()
acct, err := h.account()

View File

@@ -31,11 +31,6 @@ func (Unsubscribe) Aliases() []string {
return []string{"unsubscribe"}
}
// Complete returns a list of completions
func (Unsubscribe) Complete(args []string) []string {
return nil
}
// Execute runs the Unsubscribe command
func (u Unsubscribe) Execute(args []string) error {
editHeaders := (config.Compose.EditHeaders || u.Edit) && !u.NoEdit

View File

@@ -14,10 +14,6 @@ func (Close) Aliases() []string {
return []string{"close"}
}
func (Close) Complete(args []string) []string {
return nil
}
func (Close) Execute(args []string) error {
mv, _ := app.SelectedTabContent().(*app.MessageViewer)
app.RemoveTab(mv, true)

View File

@@ -16,10 +16,6 @@ func (NextPrevPart) Aliases() []string {
return []string{"next-part", "prev-part"}
}
func (NextPrevPart) Complete(args []string) []string {
return nil
}
func (np NextPrevPart) Execute(args []string) error {
mv, _ := app.SelectedTabContent().(*app.MessageViewer)
for n := 0; n < np.Offset; n++ {

View File

@@ -39,10 +39,6 @@ func (NextPrevMsg) Aliases() []string {
return []string{"next", "next-message", "prev", "prev-message"}
}
func (NextPrevMsg) Complete(args []string) []string {
return nil
}
func (np NextPrevMsg) Execute(args []string) error {
cmd := account.NextPrevMsg{Amount: np.Amount, Percent: np.Percent}
err := cmd.Execute(args)

View File

@@ -11,7 +11,7 @@ import (
)
type OpenLink struct {
Url *url.URL `opt:"url" action:"ParseUrl"`
Url *url.URL `opt:"url" action:"ParseUrl" complete:"CompleteUrl"`
Cmd string `opt:"..." required:"false"`
}
@@ -23,11 +23,11 @@ func (OpenLink) Aliases() []string {
return []string{"open-link"}
}
func (OpenLink) Complete(args []string) []string {
func (*OpenLink) CompleteUrl(arg string) []string {
mv := app.SelectedTabContent().(*app.MessageViewer)
if mv != nil {
if p := mv.SelectedMessagePart(); p != nil {
return commands.CompletionFromList(p.Links, args)
return commands.CompletionFromList(p.Links, arg)
}
}
return nil

View File

@@ -29,10 +29,6 @@ func (Open) Aliases() []string {
return []string{"open"}
}
func (Open) Complete(args []string) []string {
return nil
}
func (o Open) Execute(args []string) error {
mv := app.SelectedTabContent().(*app.MessageViewer)
if mv == nil {

View File

@@ -22,7 +22,7 @@ type Save struct {
CreateDirs bool `opt:"-p"`
Attachments bool `opt:"-a"`
AllAttachments bool `opt:"-A"`
Path string `opt:"..." required:"false" metavar:"<path>"`
Path string `opt:"path" required:"false" complete:"CompletePath"`
}
func init() {
@@ -37,14 +37,12 @@ func (Save) Aliases() []string {
return []string{"save"}
}
func (s Save) Complete(args []string) []string {
trimmed := commands.Operands(args, s.Options())
path := strings.Join(trimmed, " ")
func (*Save) CompletePath(arg string) []string {
defaultPath := config.General.DefaultSavePath
if defaultPath != "" && !isAbsPath(path) {
path = filepath.Join(defaultPath, path)
if defaultPath != "" && !isAbsPath(arg) {
arg = filepath.Join(defaultPath, arg)
}
return commands.CompletePath(xdg.ExpandHome(path))
return commands.CompletePath(xdg.ExpandHome(arg))
}
func (s Save) Execute(args []string) error {

View File

@@ -14,10 +14,6 @@ func (ToggleHeaders) Aliases() []string {
return []string{"toggle-headers"}
}
func (ToggleHeaders) Complete(args []string) []string {
return nil
}
func (ToggleHeaders) Execute(args []string) error {
mv, _ := app.SelectedTabContent().(*app.MessageViewer)
mv.ToggleHeaders()

View File

@@ -15,10 +15,6 @@ func (ToggleKeyPassthrough) Aliases() []string {
return []string{"toggle-key-passthrough"}
}
func (ToggleKeyPassthrough) Complete(args []string) []string {
return nil
}
func (ToggleKeyPassthrough) Execute(args []string) error {
mv, _ := app.SelectedTabContent().(*app.MessageViewer)
keyPassthroughEnabled := mv.ToggleKeyPassthrough()

View File

@@ -16,10 +16,6 @@ func (NewAccount) Aliases() []string {
return []string{"new-account"}
}
func (NewAccount) Complete(args []string) []string {
return nil
}
func (n NewAccount) Execute(args []string) error {
wizard := app.NewAccountWizard()
wizard.ConfigureTemporaryAccount(n.Temp)

View File

@@ -16,10 +16,6 @@ func (NextPrevTab) Aliases() []string {
return []string{"next-tab", "prev-tab"}
}
func (NextPrevTab) Complete(args []string) []string {
return nil
}
func (np NextPrevTab) Execute(args []string) error {
for n := 0; n < np.Offset; n++ {
if args[0] == "prev-tab" {

View File

@@ -1,134 +0,0 @@
package commands
import (
"strings"
)
type completionType int
const (
NONE completionType = iota
COMMAND
OPERAND
SHORT_OPTION
OPTION_ARGUMENT
)
type parser struct {
tokens []string
optind int
spec string
space bool
kind completionType
flag string
arg string
err error
}
func newParser(cmd, spec string, spaceTerminated bool) (*parser, error) {
args, err := splitCmd(cmd)
if err != nil {
return nil, err
}
p := &parser{
tokens: args,
optind: 0,
spec: spec,
space: spaceTerminated,
kind: NONE,
flag: "",
arg: "",
err: nil,
}
state := command
for state != nil {
state = state(p)
}
return p, p.err
}
func (p *parser) empty() bool {
return len(p.tokens) == 0
}
func (p *parser) peek() string {
return p.tokens[0]
}
func (p *parser) advance() string {
if p.empty() {
return ""
}
tok := p.tokens[0]
p.tokens = p.tokens[1:]
p.optind++
return tok
}
func (p *parser) set(t completionType) {
p.kind = t
}
func (p *parser) hasArgument() bool {
n := len(p.flag)
if n > 0 {
s := string(p.flag[n-1]) + ":"
return strings.Contains(p.spec, s)
}
return false
}
type stateFn func(*parser) stateFn
func command(p *parser) stateFn {
p.set(COMMAND)
p.advance()
return peek(p)
}
func peek(p *parser) stateFn {
if p.empty() {
if p.space {
return operand
}
return nil
}
if p.spec == "" {
return operand
}
s := p.peek()
switch {
case s == "--":
p.advance()
case strings.HasPrefix(s, "-"):
return short_option
}
return operand
}
func short_option(p *parser) stateFn {
p.set(SHORT_OPTION)
tok := p.advance()
p.flag = tok[1:]
if p.hasArgument() {
return option_argument
}
return peek(p)
}
func option_argument(p *parser) stateFn {
p.set(OPTION_ARGUMENT)
p.arg = p.advance()
if p.empty() && len(p.arg) == 0 {
return nil
}
return peek(p)
}
func operand(p *parser) stateFn {
p.set(OPERAND)
return nil
}

View File

@@ -1,258 +0,0 @@
package commands
import (
"testing"
)
var parserTests = []struct {
name string
cmd string
wantType completionType
wantFlag string
wantArg string
wantOptind int
}{
{
name: "empty command",
cmd: "",
wantType: COMMAND,
wantFlag: "",
wantArg: "",
wantOptind: 0,
},
{
name: "command only",
cmd: "cmd",
wantType: COMMAND,
wantFlag: "",
wantArg: "",
wantOptind: 1,
},
{
name: "with space",
cmd: "cmd ",
wantType: OPERAND,
wantFlag: "",
wantArg: "",
wantOptind: 1,
},
{
name: "with two spaces",
cmd: "cmd ",
wantType: OPERAND,
wantFlag: "",
wantArg: "",
wantOptind: 1,
},
{
name: "with single option flag",
cmd: "cmd -",
wantType: SHORT_OPTION,
wantFlag: "",
wantArg: "",
wantOptind: 2,
},
{
name: "with single option flag two spaces",
cmd: "cmd -",
wantType: SHORT_OPTION,
wantFlag: "",
wantArg: "",
wantOptind: 2,
},
{
name: "with single option flag completed",
cmd: "cmd -a",
wantType: SHORT_OPTION,
wantFlag: "a",
wantArg: "",
wantOptind: 2,
},
{
name: "with single option flag completed and space",
cmd: "cmd -a ",
wantType: OPERAND,
wantFlag: "a",
wantArg: "",
wantOptind: 2,
},
{
name: "with single option flag completed and two spaces",
cmd: "cmd -a ",
wantType: OPERAND,
wantFlag: "a",
wantArg: "",
wantOptind: 2,
},
{
name: "with two single option flag completed",
cmd: "cmd -b -a",
wantType: SHORT_OPTION,
wantFlag: "a",
wantArg: "",
wantOptind: 3,
},
{
name: "with two single option flag combined",
cmd: "cmd -ab",
wantType: SHORT_OPTION,
wantFlag: "ab",
wantArg: "",
wantOptind: 2,
},
{
name: "with two single option flag and space",
cmd: "cmd -ab ",
wantType: OPERAND,
wantFlag: "ab",
wantArg: "",
wantOptind: 2,
},
{
name: "with mandatory option flag",
cmd: "cmd -f",
wantType: OPTION_ARGUMENT,
wantFlag: "f",
wantArg: "",
wantOptind: 2,
},
{
name: "with mandatory option flag and space",
cmd: "cmd -f ",
wantType: OPTION_ARGUMENT,
wantFlag: "f",
wantArg: "",
wantOptind: 2,
},
{
name: "with mandatory option flag and two spaces",
cmd: "cmd -f ",
wantType: OPTION_ARGUMENT,
wantFlag: "f",
wantArg: "",
wantOptind: 2,
},
{
name: "with mandatory option flag and completed",
cmd: "cmd -f a",
wantType: OPTION_ARGUMENT,
wantFlag: "f",
wantArg: "a",
wantOptind: 3,
},
{
name: "with mandatory option flag and completed quote",
cmd: "cmd -f 'a b'",
wantType: OPTION_ARGUMENT,
wantFlag: "f",
wantArg: "a b",
wantOptind: 3,
},
{
name: "with mandatory option flag and operand",
cmd: "cmd -f 'a b' hello",
wantType: OPERAND,
wantFlag: "f",
wantArg: "a b",
wantOptind: 3,
},
{
name: "with mandatory option flag and two spaces between",
cmd: "cmd -f a",
wantType: OPTION_ARGUMENT,
wantFlag: "f",
wantArg: "a",
wantOptind: 3,
},
{
name: "with mandatory option flag and more spaces",
cmd: "cmd -f a ",
wantType: OPERAND,
wantFlag: "f",
wantArg: "a",
wantOptind: 3,
},
{
name: "with template data",
cmd: "cmd -a {{if .Size}} hello {{else}} {{end}}",
wantType: OPERAND,
wantFlag: "a",
wantArg: "",
wantOptind: 2,
},
{
name: "with operand",
cmd: "cmd -ab /tmp/aerc-",
wantType: OPERAND,
wantFlag: "ab",
wantArg: "",
wantOptind: 2,
},
{
name: "with operand indicator",
cmd: "cmd -ab -- /tmp/aerc-",
wantType: OPERAND,
wantFlag: "ab",
wantArg: "",
wantOptind: 3,
},
{
name: "hyphen connected command",
cmd: "cmd-dmc",
wantType: COMMAND,
wantFlag: "",
wantArg: "",
wantOptind: 1,
},
{
name: "incomplete hyphen connected command",
cmd: "cmd-",
wantType: COMMAND,
wantFlag: "",
wantArg: "",
wantOptind: 1,
},
{
name: "hyphen connected command with option",
cmd: "cmd-dmc -a",
wantType: SHORT_OPTION,
wantFlag: "a",
wantArg: "",
wantOptind: 2,
},
}
func TestCommands_Parser(t *testing.T) {
for i, test := range parserTests {
n := len(test.cmd)
spaceTerminated := n > 0 && test.cmd[n-1] == ' '
parser, err := newParser(test.cmd, "abf:", spaceTerminated)
if err != nil {
t.Errorf("parser error: %v", err)
}
if test.wantType != parser.kind {
t.Errorf("test %d '%s': completion type does not match: "+
"want %d, but got %d", i, test.cmd, test.wantType,
parser.kind)
}
if test.wantFlag != parser.flag {
t.Errorf("test %d '%s': flag does not match: "+
"want %s, but got %s", i, test.cmd, test.wantFlag,
parser.flag)
}
if test.wantArg != parser.arg {
t.Errorf("test %d '%s': arg does not match: "+
"want %s, but got %s", i, test.cmd, test.wantArg,
parser.arg)
}
if test.wantOptind != parser.optind {
t.Errorf("test %d '%s': optind does not match: "+
"want %d, but got %d", i, test.cmd, test.wantOptind,
parser.optind)
}
}
}

View File

@@ -14,10 +14,6 @@ func (PinTab) Aliases() []string {
return []string{"pin-tab", "unpin-tab"}
}
func (PinTab) Complete(args []string) []string {
return nil
}
func (PinTab) Execute(args []string) error {
switch args[0] {
case "pin-tab":

View File

@@ -1,8 +1,6 @@
package commands
import (
"strings"
"git.sr.ht/~rjarry/go-opt"
"git.sr.ht/~rjarry/aerc/app"
@@ -10,7 +8,7 @@ import (
type Prompt struct {
Text string `opt:"text"`
Cmd []string `opt:"..."`
Cmd []string `opt:"..." complete:"CompleteCommand"`
}
func init() {
@@ -21,56 +19,8 @@ func (Prompt) Aliases() []string {
return []string{"prompt"}
}
func (Prompt) Complete(args []string) []string {
argc := len(args)
if argc == 0 {
return nil
}
hascommand := argc > 2
if argc == 1 {
args = append(args, "")
}
cmd := GlobalCommands.ByName(args[1])
var cs []string
if cmd != nil {
cs = cmd.Complete(args[2:])
hascommand = true
} else {
if hascommand {
return nil
}
cs = GlobalCommands.Names()
}
if cs == nil {
return nil
}
var b strings.Builder
// it seems '' quoting is enough
// to keep quoted arguments in one piece
b.WriteRune('\'')
b.WriteString(args[0])
b.WriteRune('\'')
b.WriteRune(' ')
if hascommand {
b.WriteString(args[1])
b.WriteRune(' ')
}
src := b.String()
b.Reset()
rs := make([]string, 0, len(cs))
for _, c := range cs {
b.WriteString(src)
b.WriteString(c)
rs = append(rs, b.String())
b.Reset()
}
return rs
func (*Prompt) CompleteCommand(arg string) []string {
return CompletionFromList(GlobalCommands.Names(), arg)
}
func (p Prompt) Execute(args []string) error {

View File

@@ -17,10 +17,6 @@ func (PrintWorkDir) Aliases() []string {
return []string{"pwd"}
}
func (PrintWorkDir) Complete(args []string) []string {
return nil
}
func (PrintWorkDir) Execute(args []string) error {
pwd, err := os.Getwd()
if err != nil {

View File

@@ -18,10 +18,6 @@ func (Quit) Aliases() []string {
return []string{"quit", "exit"}
}
func (Quit) Complete(args []string) []string {
return nil
}
type ErrorExit int
func (err ErrorExit) Error() string {

View File

@@ -19,10 +19,6 @@ func (SendKeys) Aliases() []string {
return []string{"send-keys"}
}
func (SendKeys) Complete(args []string) []string {
return nil
}
func (s SendKeys) Execute(args []string) error {
tab, ok := app.SelectedTabContent().(app.HasTerminal)
if !ok {

View File

@@ -12,10 +12,6 @@ func (Suspend) Aliases() []string {
return []string{"suspend"}
}
func (Suspend) Complete(args []string) []string {
return nil
}
func (Suspend) Execute(args []string) error {
ui.QueueSuspend()
return nil

View File

@@ -21,10 +21,6 @@ func (Term) Aliases() []string {
return []string{"terminal", "term"}
}
func (Term) Complete(args []string) []string {
return nil
}
func (t Term) Execute(args []string) error {
if len(t.Cmd) == 0 {
shell, err := loginshell.Shell()

View File

@@ -14,10 +14,6 @@ func (Close) Aliases() []string {
return []string{"close"}
}
func (Close) Complete(args []string) []string {
return nil
}
func (Close) Execute(args []string) error {
term, _ := app.SelectedTabContent().(*app.Terminal)
term.Close()

View File

@@ -18,6 +18,7 @@ import (
"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"
)
@@ -93,7 +94,7 @@ func CompletePath(path string) []string {
}
if !strings.HasPrefix(path, ".") && !strings.Contains(path, "/.") {
log.Debugf("removing hidden files from glob results")
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 {
@@ -107,11 +108,13 @@ func CompletePath(path string) []string {
for i, m := range matches {
if isDir(m) {
matches[i] = m + "/"
m += "/"
}
matches[i] = opt.QuoteArg((xdg.TildeHome(m)))
}
sort.Strings(matches)
return matches
}
@@ -122,11 +125,11 @@ func CompletePath(path string) []string {
if isDir(f) {
f += "/"
}
files[i] = f
files[i] = opt.QuoteArg((xdg.TildeHome(f)))
}
sort.Strings(files)
return files
}
@@ -227,16 +230,16 @@ func MsgInfoFromUids(store *lib.MessageStore, uids []uint32, statusInfo func(str
// FilterList takes a list of valid completions and filters it, either
// by case smart prefix, or by fuzzy matching, prepending "prefix" to each completion
func FilterList(valid []string, search, prefix string, isFuzzy bool) []string {
out := make([]string, 0)
func FilterList(valid []string, search, prefix, suffix string, isFuzzy bool) []string {
out := make([]string, 0, len(valid))
if isFuzzy {
for _, v := range fuzzy.RankFindFold(search, valid) {
out = append(out, prefix+v.Target)
out = append(out, opt.QuoteArg(prefix+v.Target+suffix))
}
} else {
for _, v := range valid {
if hasCaseSmartPrefix(v, search) {
out = append(out, prefix+v)
out = append(out, opt.QuoteArg(prefix+v+suffix))
}
}
}

View File

@@ -8,7 +8,8 @@ import (
)
type Zoxide struct {
Target string `opt:"..." default:"~" metavar:"<folder> | <query>..."`
Target string `opt:"folder" default:"~" complete:"CompleteFolder"`
Args []string `opt:"..." required:"false" metavar:"<query>..."`
}
func ZoxideAdd(arg string) error {
@@ -36,8 +37,8 @@ func (Zoxide) Aliases() []string {
return []string{"z"}
}
func (Zoxide) Complete(args []string) []string {
return ChangeDirectory{}.Complete(args)
func (*Zoxide) CompleteFolder(arg string) []string {
return GetFolders(arg)
}
// Execute calls zoxide add and query and delegates actually changing the

View File

@@ -143,7 +143,7 @@ func getCompletions(cmdline string) ([]string, string) {
for _, set := range cmds {
for _, n := range set.Names() {
if strings.HasPrefix(n, cmdline) {
completions = append(completions, n)
completions = append(completions, n+" ")
}
}
}