From abe228b14d97d8d47e8ff4406de387fac45cfe68 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Sun, 22 Oct 2023 23:23:18 +0200 Subject: 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 Reviewed-by: Koni Marti Tested-by: Moritz Poldrack Tested-by: Inwit --- commands/account/cf.go | 6 +- commands/account/check-mail.go | 4 - commands/account/clear.go | 4 - commands/account/compose.go | 11 +- commands/account/connection.go | 4 - commands/account/expand-folder.go | 4 - commands/account/export-mbox.go | 6 +- commands/account/import-mbox.go | 7 +- commands/account/mkdir.go | 29 +--- commands/account/next-folder.go | 4 - commands/account/next-result.go | 4 - commands/account/next.go | 4 - commands/account/recover.go | 7 +- commands/account/rmdir.go | 4 - commands/account/search.go | 41 ++--- commands/account/select.go | 4 - commands/account/sort.go | 56 ++----- commands/account/split.go | 4 - commands/account/view.go | 4 - commands/cd.go | 7 +- commands/choose.go | 4 - commands/commands.go | 164 +++++------------- commands/compose/abort.go | 4 - commands/compose/attach-key.go | 4 - commands/compose/attach.go | 13 +- commands/compose/cc-bcc.go | 4 - commands/compose/detach.go | 7 +- commands/compose/edit.go | 4 - commands/compose/encrypt.go | 4 - commands/compose/header.go | 6 +- commands/compose/multipart.go | 7 +- commands/compose/next-field.go | 4 - commands/compose/postpone.go | 18 +- commands/compose/send.go | 19 +-- commands/compose/sign.go | 4 - commands/compose/switch.go | 7 +- commands/ct.go | 10 +- commands/eml.go | 7 +- commands/exec.go | 4 - commands/help.go | 6 +- commands/move-tab.go | 4 - commands/msg/archive.go | 7 +- commands/msg/copy.go | 6 +- commands/msg/delete.go | 4 - commands/msg/envelope.go | 4 - commands/msg/fold.go | 4 - commands/msg/forward.go | 7 +- commands/msg/invite.go | 4 - commands/msg/mark.go | 4 - commands/msg/modify-labels.go | 6 +- commands/msg/move.go | 6 +- commands/msg/pipe.go | 4 - commands/msg/read.go | 13 +- commands/msg/recall.go | 4 - commands/msg/reply.go | 7 +- commands/msg/toggle-thread-context.go | 4 - commands/msg/toggle-threads.go | 4 - commands/msg/unsubscribe.go | 5 - commands/msgview/close.go | 4 - commands/msgview/next-part.go | 4 - commands/msgview/next.go | 4 - commands/msgview/open-link.go | 6 +- commands/msgview/open.go | 4 - commands/msgview/save.go | 12 +- commands/msgview/toggle-headers.go | 4 - commands/msgview/toggle-key-passthrough.go | 4 - commands/new-account.go | 4 - commands/next-tab.go | 4 - commands/parser.go | 134 --------------- commands/parser_test.go | 258 ----------------------------- commands/pin-tab.go | 4 - commands/prompt.go | 56 +------ commands/pwd.go | 4 - commands/quit.go | 4 - commands/send-keys.go | 4 - commands/suspend.go | 4 - commands/term.go | 4 - commands/terminal/close.go | 4 - commands/util.go | 19 ++- commands/z.go | 7 +- main.go | 2 +- 81 files changed, 197 insertions(+), 968 deletions(-) delete mode 100644 commands/parser.go delete mode 100644 commands/parser_test.go diff --git a/commands/account/cf.go b/commands/account/cf.go index 59203a89..d73d4978 100644 --- a/commands/account/cf.go +++ b/commands/account/cf.go @@ -11,7 +11,7 @@ import ( var history map[string]string type ChangeFolder struct { - Folder string `opt:"..." metavar:""` + 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 { diff --git a/commands/account/check-mail.go b/commands/account/check-mail.go index a8d80a66..d31b2648 100644 --- a/commands/account/check-mail.go +++ b/commands/account/check-mail.go @@ -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 { diff --git a/commands/account/clear.go b/commands/account/clear.go index fd209d07..dec6bcd2 100644 --- a/commands/account/clear.go +++ b/commands/account/clear.go @@ -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 { diff --git a/commands/account/compose.go b/commands/account/compose.go index 81eb3de0..fe86a179 100644 --- a/commands/account/compose.go +++ b/commands/account/compose.go @@ -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 { diff --git a/commands/account/connection.go b/commands/account/connection.go index b9cd887b..d633c1ce 100644 --- a/commands/account/connection.go +++ b/commands/account/connection.go @@ -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 { diff --git a/commands/account/expand-folder.go b/commands/account/expand-folder.go index f26da70a..c264872a 100644 --- a/commands/account/expand-folder.go +++ b/commands/account/expand-folder.go @@ -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 { diff --git a/commands/account/export-mbox.go b/commands/account/export-mbox.go index 00e03ca6..14d9290e 100644 --- a/commands/account/export-mbox.go +++ b/commands/account/export-mbox.go @@ -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 { diff --git a/commands/account/import-mbox.go b/commands/account/import-mbox.go index b3ad2a08..774dfa8f 100644 --- a/commands/account/import-mbox.go +++ b/commands/account/import-mbox.go @@ -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 { diff --git a/commands/account/mkdir.go b/commands/account/mkdir.go index 9beeb01a..3e546ace 100644 --- a/commands/account/mkdir.go +++ b/commands/account/mkdir.go @@ -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 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 { diff --git a/commands/account/next-folder.go b/commands/account/next-folder.go index 1b5651c8..f44abdc1 100644 --- a/commands/account/next-folder.go +++ b/commands/account/next-folder.go @@ -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 { diff --git a/commands/account/next-result.go b/commands/account/next-result.go index e841899f..d624e559 100644 --- a/commands/account/next-result.go +++ b/commands/account/next-result.go @@ -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 { diff --git a/commands/account/next.go b/commands/account/next.go index 14679b1b..142f6151 100644 --- a/commands/account/next.go +++ b/commands/account/next.go @@ -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 { diff --git a/commands/account/recover.go b/commands/account/recover.go index dae3d807..cba6e0cb 100644 --- a/commands/account/recover.go +++ b/commands/account/recover.go @@ -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 { diff --git a/commands/account/rmdir.go b/commands/account/rmdir.go index 36bfc699..398f375f 100644 --- a/commands/account/rmdir.go +++ b/commands/account/rmdir.go @@ -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 { diff --git a/commands/account/search.go b/commands/account/search.go index bb5617c0..ca1b9684 100644 --- a/commands/account/search.go +++ b/commands/account/search.go @@ -23,12 +23,12 @@ type SearchFilter struct { Body bool `opt:"-b"` All bool `opt:"-a"` Headers textproto.MIMEHeader `opt:"-H" action:"ParseHeader" metavar:"
:"` - 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 { diff --git a/commands/account/select.go b/commands/account/select.go index efd8f53b..884b4bce 100644 --- a/commands/account/select.go +++ b/commands/account/select.go @@ -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 { diff --git a/commands/account/sort.go b/commands/account/sort.go index 2adfbf19..ccccab25 100644 --- a/commands/account/sort.go +++ b/commands/account/sort.go @@ -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 { diff --git a/commands/account/split.go b/commands/account/split.go index 4774297f..8690d99a 100644 --- a/commands/account/split.go +++ b/commands/account/split.go @@ -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 { diff --git a/commands/account/view.go b/commands/account/view.go index 701a7738..0cdc175d 100644 --- a/commands/account/view.go +++ b/commands/account/view.go @@ -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 { diff --git a/commands/cd.go b/commands/cd.go index 6620e732..d2dc5310 100644 --- a/commands/cd.go +++ b/commands/cd.go @@ -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 { diff --git a/commands/choose.go b/commands/choose.go index 4e2007fd..19958dce 100644 --- a/commands/choose.go +++ b/commands/choose.go @@ -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]) diff --git a/commands/commands.go b/commands/commands.go index d76194af..9c193018 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -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 + } + } + } + names := make([]string, len(templates)) + for n := range templates { + names = append(names, n) } - return FilterList(valid, args[0], "", app.SelectedAccountUiConfig().FuzzyComplete) + 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 = "" - } - trimmed := strings.TrimLeft(last, "+-") - - var prev string - if len(others) > 0 { - prev = others + " " + 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, "+-") } - 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 -} diff --git a/commands/compose/abort.go b/commands/compose/abort.go index 4bacb9c3..cde43f46 100644 --- a/commands/compose/abort.go +++ b/commands/compose/abort.go @@ -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) diff --git a/commands/compose/attach-key.go b/commands/compose/attach-key.go index e81a6a5a..6ba5eae0 100644 --- a/commands/compose/attach-key.go +++ b/commands/compose/attach-key.go @@ -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()) diff --git a/commands/compose/attach.go b/commands/compose/attach.go index 3acf28c2..49d63bb8 100644 --- a/commands/compose/attach.go +++ b/commands/compose/attach.go @@ -23,7 +23,8 @@ import ( type Attach struct { Menu bool `opt:"-m"` Name string `opt:"-r"` - Path string `opt:"..." metavar:"" 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 { diff --git a/commands/compose/cc-bcc.go b/commands/compose/cc-bcc.go index aeb6af97..dd5e9614 100644 --- a/commands/compose/cc-bcc.go +++ b/commands/compose/cc-bcc.go @@ -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) diff --git a/commands/compose/detach.go b/commands/compose/detach.go index a2996516..91cf2a58 100644 --- a/commands/compose/detach.go +++ b/commands/compose/detach.go @@ -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 { diff --git a/commands/compose/edit.go b/commands/compose/edit.go index 80f4e6f4..1929f45a 100644 --- a/commands/compose/edit.go +++ b/commands/compose/edit.go @@ -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 { diff --git a/commands/compose/encrypt.go b/commands/compose/encrypt.go index 3121dff0..ee902f6e 100644 --- a/commands/compose/encrypt.go +++ b/commands/compose/encrypt.go @@ -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()) diff --git a/commands/compose/header.go b/commands/compose/header.go index aaa14e43..afc27e92 100644 --- a/commands/compose/header.go +++ b/commands/compose/header.go @@ -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 { diff --git a/commands/compose/multipart.go b/commands/compose/multipart.go index 96941062..5b701342 100644 --- a/commands/compose/multipart.go +++ b/commands/compose/multipart.go @@ -11,7 +11,7 @@ import ( type Multipart struct { Remove bool `opt:"-d"` - Mime string `opt:"mime" metavar:""` + Mime string `opt:"mime" metavar:"" 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 { diff --git a/commands/compose/next-field.go b/commands/compose/next-field.go index 88fbb03d..396c1ce1 100644 --- a/commands/compose/next-field.go +++ b/commands/compose/next-field.go @@ -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" { diff --git a/commands/compose/postpone.go b/commands/compose/postpone.go index 767e759c..3ce9cc84 100644 --- a/commands/compose/postpone.go +++ b/commands/compose/postpone.go @@ -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 { diff --git a/commands/compose/send.go b/commands/compose/send.go index e3672471..6f905628 100644 --- a/commands/compose/send.go +++ b/commands/compose/send.go @@ -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 { diff --git a/commands/compose/sign.go b/commands/compose/sign.go index e30fca05..faeceed4 100644 --- a/commands/compose/sign.go +++ b/commands/compose/sign.go @@ -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) diff --git a/commands/compose/switch.go b/commands/compose/switch.go index 0c027a41..637099b5 100644 --- a/commands/compose/switch.go +++ b/commands/compose/switch.go @@ -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:"" 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 { diff --git a/commands/ct.go b/commands/ct.go index 8a6bb063..2d057b4f 100644 --- a/commands/ct.go +++ b/commands/ct.go @@ -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 { diff --git a/commands/eml.go b/commands/eml.go index adacd05b..fe735243 100644 --- a/commands/eml.go +++ b/commands/eml.go @@ -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 { diff --git a/commands/exec.go b/commands/exec.go index 4c2f3d1b..2a3ed5f5 100644 --- a/commands/exec.go +++ b/commands/exec.go @@ -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() diff --git a/commands/help.go b/commands/help.go index b2bcdf7c..07332303 100644 --- a/commands/help.go +++ b/commands/help.go @@ -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 { diff --git a/commands/move-tab.go b/commands/move-tab.go index 23580f2d..7ebe269c 100644 --- a/commands/move-tab.go +++ b/commands/move-tab.go @@ -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 diff --git a/commands/msg/archive.go b/commands/msg/archive.go index f4d6e3be..34cba8b8 100644 --- a/commands/msg/archive.go +++ b/commands/msg/archive.go @@ -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 { diff --git a/commands/msg/copy.go b/commands/msg/copy.go index 4109ef99..77c9ade1 100644 --- a/commands/msg/copy.go +++ b/commands/msg/copy.go @@ -10,7 +10,7 @@ import ( type Copy struct { CreateFolders bool `opt:"-p"` - Folder string `opt:"..." metavar:""` + 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 { diff --git a/commands/msg/delete.go b/commands/msg/delete.go index 49463abc..1a1a130c 100644 --- a/commands/msg/delete.go +++ b/commands/msg/delete.go @@ -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() diff --git a/commands/msg/envelope.go b/commands/msg/envelope.go index 6da82a1e..e104648d 100644 --- a/commands/msg/envelope.go +++ b/commands/msg/envelope.go @@ -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 { diff --git a/commands/msg/fold.go b/commands/msg/fold.go index 0621c8c3..06e933d7 100644 --- a/commands/msg/fold.go +++ b/commands/msg/fold.go @@ -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() diff --git a/commands/msg/forward.go b/commands/msg/forward.go index e6e386a9..4147e8c7 100644 --- a/commands/msg/forward.go +++ b/commands/msg/forward.go @@ -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 { diff --git a/commands/msg/invite.go b/commands/msg/invite.go index 5b8558b0..12bea93e 100644 --- a/commands/msg/invite.go +++ b/commands/msg/invite.go @@ -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 { diff --git a/commands/msg/mark.go b/commands/msg/mark.go index c2c21cf2..9547548f 100644 --- a/commands/msg/mark.go +++ b/commands/msg/mark.go @@ -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 { diff --git a/commands/msg/modify-labels.go b/commands/msg/modify-labels.go index 6fdbeac4..8cc72f2a 100644 --- a/commands/msg/modify-labels.go +++ b/commands/msg/modify-labels.go @@ -9,7 +9,7 @@ import ( ) type ModifyLabels struct { - Labels []string `opt:"..." metavar:"[+-]