diff options
75 files changed, 673 insertions, 1346 deletions
diff --git a/commands/account/cf.go b/commands/account/cf.go index cd93ed29..59203a89 100644 --- a/commands/account/cf.go +++ b/commands/account/cf.go @@ -2,7 +2,6 @@ package account import ( "errors" - "strings" "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/commands" @@ -11,7 +10,9 @@ import ( var history map[string]string -type ChangeFolder struct{} +type ChangeFolder struct { + Folder string `opt:"..." metavar:"<folder>"` +} func init() { history = make(map[string]string) @@ -26,24 +27,21 @@ func (ChangeFolder) Complete(args []string) []string { return commands.GetFolders(args) } -func (ChangeFolder) Execute(args []string) error { - if len(args) == 1 { - return errors.New("Usage: cf <folder>") - } +func (c ChangeFolder) Execute(args []string) error { acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") } previous := acct.Directories().Selected() - joinedArgs := strings.Join(args[1:], " ") - if joinedArgs == "-" { + + if c.Folder == "-" { if dir, ok := history[acct.Name()]; ok { acct.Directories().Select(dir) } else { return errors.New("No previous folder to return to") } } else { - acct.Directories().Select(joinedArgs) + acct.Directories().Select(c.Folder) } history[acct.Name()] = previous diff --git a/commands/account/clear.go b/commands/account/clear.go index b67454dd..fd209d07 100644 --- a/commands/account/clear.go +++ b/commands/account/clear.go @@ -5,10 +5,11 @@ import ( "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/lib/state" - "git.sr.ht/~sircmpwn/getopt" ) -type Clear struct{} +type Clear struct { + Selected bool `opt:"-s"` +} func init() { register(Clear{}) @@ -22,7 +23,7 @@ func (Clear) Complete(args []string) []string { return nil } -func (Clear) Execute(args []string) error { +func (c Clear) Execute(args []string) error { acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") @@ -32,23 +33,7 @@ func (Clear) Execute(args []string) error { return errors.New("Cannot perform action. Messages still loading") } - clearSelected := false - opts, optind, err := getopt.Getopts(args, "s") - if err != nil { - return err - } - - for _, opt := range opts { - if opt.Option == 's' { - clearSelected = true - } - } - - if len(args) != optind { - return errors.New("Usage: clear [-s]") - } - - if clearSelected { + if c.Selected { defer store.Select(0) } store.ApplyClear() diff --git a/commands/account/compose.go b/commands/account/compose.go index 56079868..81eb3de0 100644 --- a/commands/account/compose.go +++ b/commands/account/compose.go @@ -12,15 +12,31 @@ import ( "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/config" - "git.sr.ht/~sircmpwn/getopt" ) -type Compose struct{} +type Compose struct { + Headers string `opt:"-H" action:"ParseHeader"` + Template string `opt:"-T"` + Edit bool `opt:"-e"` + NoEdit bool `opt:"-E"` + Body string `opt:"..." required:"false"` +} func init() { register(Compose{}) } +func (c *Compose) ParseHeader(arg string) error { + if strings.Contains(arg, ":") { + // ensure first colon is followed by a single space + re := regexp.MustCompile(`^(.*?):\s*(.*)`) + c.Headers += re.ReplaceAllString(arg, "$1: $2\r\n") + } else { + c.Headers += arg + ":\r\n" + } + return nil +} + func (Compose) Aliases() []string { return []string{"compose"} } @@ -29,20 +45,25 @@ func (Compose) Complete(args []string) []string { return nil } -func (Compose) Execute(args []string) error { - body, template, editHeaders, err := buildBody(args) - if err != nil { - return err +func (c Compose) Execute(args []string) error { + if c.Headers != "" { + if c.Body != "" { + c.Body = c.Headers + "\r\n" + c.Body + } else { + c.Body = c.Headers + "\r\n\r\n" + } } + if c.Template == "" { + c.Template = config.Templates.NewMessage + } + editHeaders := (config.Compose.EditHeaders || c.Edit) && !c.NoEdit + acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") } - if template == "" { - template = config.Templates.NewMessage - } - msg, err := gomail.ReadMessage(strings.NewReader(body)) + msg, err := gomail.ReadMessage(strings.NewReader(c.Body)) if errors.Is(err, io.EOF) { // completely empty msg = &gomail.Message{Body: strings.NewReader("")} } else if err != nil { @@ -52,52 +73,10 @@ func (Compose) Execute(args []string) error { composer, err := app.NewComposer(acct, acct.AccountConfig(), acct.Worker(), editHeaders, - template, &headers, nil, msg.Body) + c.Template, &headers, nil, msg.Body) if err != nil { return err } composer.Tab = app.NewTab(composer, "New email") return nil } - -func buildBody(args []string) (string, string, bool, error) { - var body, template, headers string - editHeaders := config.Compose.EditHeaders - opts, optind, err := getopt.Getopts(args, "H:T:eE") - if err != nil { - return "", "", false, err - } - for _, opt := range opts { - switch opt.Option { - case 'H': - if strings.Contains(opt.Value, ":") { - // ensure first colon is followed by a single space - re := regexp.MustCompile(`^(.*?):\s*(.*)`) - headers += re.ReplaceAllString(opt.Value, "$1: $2") + "\n" - } else { - headers += opt.Value + ":\n" - } - case 'T': - template = opt.Value - case 'e': - editHeaders = true - case 'E': - editHeaders = false - } - } - posargs := args[optind:] - if len(posargs) > 1 { - return "", "", false, errors.New("Usage: compose [-H header] [-T template] [-e|-E] [body]") - } - if len(posargs) == 1 { - body = posargs[0] - } - if headers != "" { - if len(body) > 0 { - body = headers + "\n" + body - } else { - body = headers + "\n\n" - } - } - return body, template, editHeaders, nil -} diff --git a/commands/account/connection.go b/commands/account/connection.go index 3b30dbda..b9cd887b 100644 --- a/commands/account/connection.go +++ b/commands/account/connection.go @@ -22,7 +22,7 @@ func (Connection) Complete(args []string) []string { return nil } -func (Connection) Execute(args []string) error { +func (c Connection) Execute(args []string) error { acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") diff --git a/commands/account/expand-folder.go b/commands/account/expand-folder.go index 29203adc..f26da70a 100644 --- a/commands/account/expand-folder.go +++ b/commands/account/expand-folder.go @@ -2,7 +2,6 @@ package account import ( "errors" - "fmt" "git.sr.ht/~rjarry/aerc/app" ) @@ -22,9 +21,6 @@ func (ExpandCollapseFolder) Complete(args []string) []string { } func (ExpandCollapseFolder) Execute(args []string) error { - if len(args) > 1 { - return expandCollapseFolderUsage(args[0]) - } acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") @@ -36,7 +32,3 @@ func (ExpandCollapseFolder) Execute(args []string) error { } return nil } - -func expandCollapseFolderUsage(cmd string) error { - return fmt.Errorf("Usage: %s", cmd) -} diff --git a/commands/account/export-mbox.go b/commands/account/export-mbox.go index a17b8a29..00e03ca6 100644 --- a/commands/account/export-mbox.go +++ b/commands/account/export-mbox.go @@ -16,7 +16,9 @@ import ( "git.sr.ht/~rjarry/aerc/worker/types" ) -type ExportMbox struct{} +type ExportMbox struct { + Filename string `opt:"filename"` +} func init() { register(ExportMbox{}) @@ -30,12 +32,7 @@ func (ExportMbox) Complete(args []string) []string { return commands.CompletePath(filepath.Join(args...)) } -func (ExportMbox) Execute(args []string) error { - if len(args) != 2 { - return exportFolderUsage(args[0]) - } - filename := args[1] - +func (e ExportMbox) Execute(args []string) error { acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") @@ -45,16 +42,16 @@ func (ExportMbox) Execute(args []string) error { return errors.New("No message store selected") } - fi, err := os.Stat(filename) + fi, err := os.Stat(e.Filename) if err == nil && fi.IsDir() { if path := acct.SelectedDirectory(); path != "" { if f := filepath.Base(path); f != "" { - filename += f + ".mbox" + e.Filename = filepath.Join(e.Filename, f+".mbox") } } } - app.PushStatus("Exporting to "+filename, 10*time.Second) + app.PushStatus("Exporting to "+e.Filename, 10*time.Second) // uids of messages to export var uids []uint32 @@ -85,7 +82,7 @@ func (ExportMbox) Execute(args []string) error { go func() { defer log.PanicHandler() - file, err := os.Create(filename) + file, err := os.Create(e.Filename) if err != nil { log.Errorf("failed to create file: %v", err) app.PushError(err.Error()) @@ -147,7 +144,7 @@ func (ExportMbox) Execute(args []string) error { } retries++ } - statusInfo := fmt.Sprintf("Exported %d of %d messages to %s.", ctr, total, filename) + statusInfo := fmt.Sprintf("Exported %d of %d messages to %s.", ctr, total, e.Filename) app.PushStatus(statusInfo, 10*time.Second) log.Debugf(statusInfo) }() @@ -155,10 +152,6 @@ func (ExportMbox) Execute(args []string) error { return nil } -func exportFolderUsage(cmd string) error { - return fmt.Errorf("Usage: %s <filename>", cmd) -} - func sortMarkedUids(marked []uint32, store *lib.MessageStore) ([]uint32, error) { lookup := map[uint32]bool{} for _, uid := range marked { diff --git a/commands/account/import-mbox.go b/commands/account/import-mbox.go index 4ede8c26..b3ad2a08 100644 --- a/commands/account/import-mbox.go +++ b/commands/account/import-mbox.go @@ -18,7 +18,9 @@ import ( "git.sr.ht/~rjarry/aerc/worker/types" ) -type ImportMbox struct{} +type ImportMbox struct { + Filename string `opt:"filename"` +} func init() { register(ImportMbox{}) @@ -32,12 +34,7 @@ func (ImportMbox) Complete(args []string) []string { return commands.CompletePath(filepath.Join(args...)) } -func (ImportMbox) Execute(args []string) error { - if len(args) != 2 { - return importFolderUsage(args[0]) - } - filename := args[1] - +func (i ImportMbox) Execute(args []string) error { acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") @@ -54,10 +51,10 @@ func (ImportMbox) Execute(args []string) error { importFolder := func() { defer log.PanicHandler() - statusInfo := fmt.Sprintln("Importing", filename, "to folder", folder) + statusInfo := fmt.Sprintln("Importing", i.Filename, "to folder", folder) app.PushStatus(statusInfo, 10*time.Second) log.Debugf(statusInfo) - f, err := os.Open(filename) + f, err := os.Open(i.Filename) if err != nil { app.PushError(err.Error()) return @@ -147,7 +144,3 @@ func (ImportMbox) Execute(args []string) error { return nil } - -func importFolderUsage(cmd string) error { - return fmt.Errorf("Usage: %s <filename>", cmd) -} diff --git a/commands/account/mkdir.go b/commands/account/mkdir.go index 7a1f7b9e..9beeb01a 100644 --- a/commands/account/mkdir.go +++ b/commands/account/mkdir.go @@ -9,7 +9,9 @@ import ( "git.sr.ht/~rjarry/aerc/worker/types" ) -type MakeDir struct{} +type MakeDir struct { + Folder string `opt:"..." metavar:"<folder>"` +} func init() { register(MakeDir{}) @@ -41,22 +43,18 @@ func (MakeDir) Complete(args []string) []string { return inboxes } -func (MakeDir) Execute(args []string) error { - if len(args) == 0 { - return errors.New("Usage: :mkdir <name>") - } +func (m MakeDir) Execute(args []string) error { acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") } - name := strings.Join(args[1:], " ") acct.Worker().PostAction(&types.CreateDirectory{ - Directory: name, + Directory: m.Folder, }, func(msg types.WorkerMessage) { switch msg := msg.(type) { case *types.Done: app.PushStatus("Directory created.", 10*time.Second) - acct.Directories().Select(name) + acct.Directories().Select(m.Folder) case *types.Error: app.PushError(msg.Error.Error()) } diff --git a/commands/account/next-folder.go b/commands/account/next-folder.go index 2b23e6da..1b5651c8 100644 --- a/commands/account/next-folder.go +++ b/commands/account/next-folder.go @@ -2,13 +2,13 @@ package account import ( "errors" - "fmt" - "strconv" "git.sr.ht/~rjarry/aerc/app" ) -type NextPrevFolder struct{} +type NextPrevFolder struct { + Offset int `opt:"n" default:"1"` +} func init() { register(NextPrevFolder{}) @@ -22,32 +22,15 @@ func (NextPrevFolder) Complete(args []string) []string { return nil } -func (NextPrevFolder) Execute(args []string) error { - if len(args) > 2 { - return nextPrevFolderUsage(args[0]) - } - var ( - n int = 1 - err error - ) - if len(args) > 1 { - n, err = strconv.Atoi(args[1]) - if err != nil { - return nextPrevFolderUsage(args[0]) - } - } +func (np NextPrevFolder) Execute(args []string) error { acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") } if args[0] == "prev-folder" { - acct.Directories().NextPrev(-n) + acct.Directories().NextPrev(-np.Offset) } else { - acct.Directories().NextPrev(n) + acct.Directories().NextPrev(np.Offset) } return nil } - -func nextPrevFolderUsage(cmd string) error { - return fmt.Errorf("Usage: %s [n]", cmd) -} diff --git a/commands/account/next-result.go b/commands/account/next-result.go index ef4078da..e841899f 100644 --- a/commands/account/next-result.go +++ b/commands/account/next-result.go @@ -2,7 +2,6 @@ package account import ( "errors" - "fmt" "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/lib/ui" @@ -23,9 +22,6 @@ func (NextPrevResult) Complete(args []string) []string { } func (NextPrevResult) Execute(args []string) error { - if len(args) > 1 { - return nextPrevResultUsage(args[0]) - } acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") @@ -45,7 +41,3 @@ func (NextPrevResult) Execute(args []string) error { } return nil } - -func nextPrevResultUsage(cmd string) error { - return fmt.Errorf("Usage: %s [<n>[%%]]", cmd) -} diff --git a/commands/account/next.go b/commands/account/next.go index fff36bd1..14679b1b 100644 --- a/commands/account/next.go +++ b/commands/account/next.go @@ -2,7 +2,6 @@ package account import ( "errors" - "fmt" "strconv" "strings" @@ -10,12 +9,28 @@ import ( "git.sr.ht/~rjarry/aerc/lib/ui" ) -type NextPrevMsg struct{} +type NextPrevMsg struct { + Amount int `opt:"n" default:"1" metavar:"<n>[%]" action:"ParseAmount"` + Percent bool +} func init() { register(NextPrevMsg{}) } +func (np *NextPrevMsg) ParseAmount(arg string) error { + if strings.HasSuffix(arg, "%") { + np.Percent = true + arg = strings.TrimSuffix(arg, "%") + } + i, err := strconv.ParseInt(arg, 10, 64) + if err != nil { + return err + } + np.Amount = int(i) + return nil +} + func (NextPrevMsg) Aliases() []string { return []string{"next", "next-message", "prev", "prev-message"} } @@ -24,42 +39,14 @@ func (NextPrevMsg) Complete(args []string) []string { return nil } -func (NextPrevMsg) Execute(args []string) error { - n, pct, err := ParseNextPrevMessage(args) - if err != nil { - return err - } +func (np NextPrevMsg) Execute(args []string) error { acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") } - return ExecuteNextPrevMessage(args, acct, pct, n) -} - -func ParseNextPrevMessage(args []string) (int, bool, error) { - if len(args) > 2 { - return 0, false, nextPrevMessageUsage(args[0]) - } - var ( - n int = 1 - err error - pct bool - ) - if len(args) > 1 { - if strings.HasSuffix(args[1], "%") { - pct = true - args[1] = args[1][:len(args[1])-1] - } - n, err = strconv.Atoi(args[1]) - if err != nil { - return 0, false, nextPrevMessageUsage(args[0]) - } - } - return n, pct, nil -} -func ExecuteNextPrevMessage(args []string, acct *app.AccountView, pct bool, n int) error { - if pct { + n := np.Amount + if np.Percent { n = int(float64(acct.Messages().Height()) * (float64(n) / 100.0)) } if args[0] == "prev-message" || args[0] == "prev" { @@ -77,7 +64,3 @@ func ExecuteNextPrevMessage(args []string, acct *app.AccountView, pct bool, n in } return nil } - -func nextPrevMessageUsage(cmd string) error { - return fmt.Errorf("Usage: %s [<n>[%%]]", cmd) -} diff --git a/commands/account/recover.go b/commands/account/recover.go index e0d1c6eb..dae3d807 100644 --- a/commands/account/recover.go +++ b/commands/account/recover.go @@ -10,10 +10,14 @@ import ( "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/commands" "git.sr.ht/~rjarry/aerc/config" - "git.sr.ht/~sircmpwn/getopt" ) -type Recover struct{} +type Recover struct { + Force bool `opt:"-f"` + Edit bool `opt:"-e"` + NoEdit bool `opt:"-E"` + File string `opt:"file"` +} func init() { register(Recover{}) @@ -40,31 +44,14 @@ func (r Recover) Complete(args []string) []string { } func (r Recover) Execute(args []string) error { - // Complete() expects to be passed only the arguments, not including the command name - if len(Recover{}.Complete(args[1:])) == 0 { - return errors.New("No messages to recover.") - } - - force := false - editHeaders := config.Compose.EditHeaders - - opts, optind, err := getopt.Getopts(args, r.Options()) + file, err := os.Open(r.File) if err != nil { return err } - for _, opt := range opts { - switch opt.Option { - case 'f': - force = true - case 'e': - editHeaders = true - case 'E': - editHeaders = false - } - } - - if len(args) <= optind { - return errors.New("Usage: recover [-f] [-E|-e] <file>") + defer file.Close() + data, err := io.ReadAll(file) + if err != nil { + return err } acct := app.SelectedAccount() @@ -72,22 +59,7 @@ func (r Recover) Execute(args []string) error { return errors.New("No account selected") } - readData := func() ([]byte, error) { - recoverFile, err := os.Open(args[optind]) - if err != nil { - return nil, err - } - defer recoverFile.Close() - data, err := io.ReadAll(recoverFile) - if err != nil { - return nil, err - } - return data, nil - } - data, err := readData() - if err != nil { - return err - } + editHeaders := (config.Compose.EditHeaders || r.Edit) && !r.NoEdit composer, err := app.NewComposer(acct, acct.AccountConfig(), acct.Worker(), editHeaders, @@ -98,8 +70,8 @@ func (r Recover) Execute(args []string) error { composer.Tab = app.NewTab(composer, "Recovered") // remove file if force flag is set - if force { - err = os.Remove(args[optind]) + if r.Force { + err = os.Remove(r.File) if err != nil { return err } diff --git a/commands/account/rmdir.go b/commands/account/rmdir.go index 3b109a67..36bfc699 100644 --- a/commands/account/rmdir.go +++ b/commands/account/rmdir.go @@ -4,13 +4,13 @@ import ( "errors" "time" - "git.sr.ht/~sircmpwn/getopt" - "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/worker/types" ) -type RemoveDir struct{} +type RemoveDir struct { + Force bool `opt:"-f"` +} func init() { register(RemoveDir{}) @@ -24,30 +24,14 @@ func (RemoveDir) Complete(args []string) []string { return nil } -func (RemoveDir) Execute(args []string) error { +func (r RemoveDir) Execute(args []string) error { acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") } - force := false - - opts, optind, err := getopt.Getopts(args, "f") - if err != nil { - return err - } - for _, opt := range opts { - if opt.Option == 'f' { - force = true - } - } - - if len(args) != optind { - return errors.New("Usage: rmdir [-f]") - } - // Check for any messages in the directory. - if !acct.Messages().Empty() && !force { + if !acct.Messages().Empty() && !r.Force { return errors.New("Refusing to remove non-empty directory; use -f") } @@ -80,7 +64,7 @@ func (RemoveDir) Execute(args []string) error { acct.Worker().PostAction(&types.RemoveDirectory{ Directory: curDir, - Quiet: force, + Quiet: r.Force, }, func(msg types.WorkerMessage) { switch msg := msg.(type) { case *types.Done: diff --git a/commands/account/search.go b/commands/account/search.go index 51bd3042..7b98d98b 100644 --- a/commands/account/search.go +++ b/commands/account/search.go @@ -12,7 +12,9 @@ import ( "git.sr.ht/~rjarry/aerc/worker/types" ) -type SearchFilter struct{} +type SearchFilter struct { + Unused struct{} `opt:"-"` +} func init() { register(SearchFilter{}) diff --git a/commands/account/select.go b/commands/account/select.go index aea17e0e..efd8f53b 100644 --- a/commands/account/select.go +++ b/commands/account/select.go @@ -2,12 +2,13 @@ package account import ( "errors" - "strconv" "git.sr.ht/~rjarry/aerc/app" ) -type SelectMessage struct{} +type SelectMessage struct { + Index int `opt:"n"` +} func init() { register(SelectMessage{}) @@ -21,20 +22,7 @@ func (SelectMessage) Complete(args []string) []string { return nil } -func (SelectMessage) Execute(args []string) error { - if len(args) != 2 { - return errors.New("Usage: :select-message <n>") - } - var ( - n int = 1 - err error - ) - if len(args) > 1 { - n, err = strconv.Atoi(args[1]) - if err != nil { - return errors.New("Usage: :select-message <n>") - } - } +func (s SelectMessage) Execute(args []string) error { acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") @@ -42,6 +30,6 @@ func (SelectMessage) Execute(args []string) error { if acct.Messages().Empty() { return nil } - acct.Messages().Select(n) + acct.Messages().Select(s.Index) return nil } diff --git a/commands/account/sort.go b/commands/account/sort.go index d9eaf16a..2adfbf19 100644 --- a/commands/account/sort.go +++ b/commands/account/sort.go @@ -11,7 +11,9 @@ import ( "git.sr.ht/~rjarry/aerc/worker/types" ) -type Sort struct{} +type Sort struct { + Unused struct{} `opt:"-"` +} func init() { register(Sort{}) diff --git a/commands/account/split.go b/commands/account/split.go index 4a17b8b2..4774297f 100644 --- a/commands/account/split.go +++ b/commands/account/split.go @@ -8,12 +8,27 @@ import ( "git.sr.ht/~rjarry/aerc/app" ) -type Split struct{} +type Split struct { + Size int `opt:"n" required:"false" action:"ParseSize"` + Delta bool +} func init() { register(Split{}) } +func (s *Split) ParseSize(arg string) error { + i, err := strconv.ParseInt(arg, 10, 64) + if err != nil { + return err + } + s.Size = int(i) + if strings.HasPrefix(arg, "+") || strings.HasPrefix(arg, "-") { + s.Delta = true + } + return nil +} + func (Split) Aliases() []string { return []string{"split", "vsplit", "hsplit"} } @@ -22,10 +37,7 @@ func (Split) Complete(args []string) []string { return nil } -func (Split) Execute(args []string) error { - if len(args) > 2 { - return errors.New("Usage: [v|h]split n") - } +func (s Split) Execute(args []string) error { acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") @@ -34,45 +46,32 @@ func (Split) Execute(args []string) error { if store == nil { return errors.New("Cannot perform action. Messages still loading") } - n := 0 - if acct.SplitSize() == 0 { - if args[0] == "split" { - n = app.SelectedAccount().Messages().Height() / 4 + + if s.Size == 0 && acct.SplitSize() == 0 { + if args[0] == "split" || args[0] == "hsplit" { + s.Size = app.SelectedAccount().Messages().Height() / 4 } else { - n = app.SelectedAccount().Messages().Width() / 2 + s.Size = app.SelectedAccount().Messages().Width() / 2 } } - - var err error - if len(args) > 1 { - delta := false - if strings.HasPrefix(args[1], "+") || strings.HasPrefix(args[1], "-") { - delta = true - } - n, err = strconv.Atoi(args[1]) - if err != nil { - return errors.New("Usage: [v|h]split n") - } - if delta { - n = acct.SplitSize() + n - acct.SetSplitSize(n) - return nil - } + if s.Delta { + acct.SetSplitSize(acct.SplitSize() + s.Size) + return nil } - if n == acct.SplitSize() { + if s.Size == acct.SplitSize() { // Repeated commands of the same size have the effect of // toggling the split - n = 0 + s.Size = 0 } - if n < 0 { + if s.Size < 0 { // Don't allow split to go negative - n = 1 + s.Size = 1 } switch args[0] { case "split", "hsplit": - return acct.Split(n) + return acct.Split(s.Size) case "vsplit": - return acct.Vsplit(n) + return acct.Vsplit(s.Size) } return nil } diff --git a/commands/account/view.go b/commands/account/view.go index 8d5bc6ed..701a7738 100644 --- a/commands/account/view.go +++ b/commands/account/view.go @@ -5,10 +5,11 @@ import ( "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/lib" - "git.sr.ht/~sircmpwn/getopt" ) -type ViewMessage struct{} +type ViewMessage struct { + Peek bool `opt:"-p"` +} func init() { register(ViewMessage{}) @@ -22,22 +23,7 @@ func (ViewMessage) Complete(args []string) []string { return nil } -func (ViewMessage) Execute(args []string) error { - peek := false - opts, optind, err := getopt.Getopts(args, "p") - if err != nil { - return err - } - - for _, opt := range opts { - if opt.Option == 'p' { - peek = true - } - } - - if len(args) != optind { - return errors.New("Usage: view-message [-p]") - } +func (v ViewMessage) Execute(args []string) error { acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") @@ -58,7 +44,7 @@ func (ViewMessage) Execute(args []string) error { app.PushError(msg.Error.Error()) return nil } - lib.NewMessageStoreView(msg, !peek && acct.UiConfig().AutoMarkRead, + lib.NewMessageStoreView(msg, !v.Peek && acct.UiConfig().AutoMarkRead, store, app.CryptoProvider(), app.DecryptKeys, func(view lib.MessageView, err error) { if err != nil { diff --git a/commands/cd.go b/commands/cd.go index ded325d5..6620e732 100644 --- a/commands/cd.go +++ b/commands/cd.go @@ -11,7 +11,9 @@ import ( var previousDir string -type ChangeDirectory struct{} +type ChangeDirectory struct { + Target string `opt:"directory" default:"~"` +} func init() { register(ChangeDirectory{}) @@ -36,25 +38,19 @@ func (ChangeDirectory) Complete(args []string) []string { return dirs } -func (ChangeDirectory) Execute(args []string) error { - if len(args) < 1 { - return errors.New("Usage: cd [directory]") - } +func (cd ChangeDirectory) Execute(args []string) error { cwd, err := os.Getwd() if err != nil { return err } - target := strings.Join(args[1:], " ") - if target == "" { - target = "~" - } else if target == "-" { + if cd.Target == "-" { if previousDir == "" { return errors.New("No previous folder to return to") } else { - target = previousDir + cd.Target = previousDir } } - target = xdg.ExpandHome(target) + target := xdg.ExpandHome(cd.Target) if err := os.Chdir(target); err == nil { previousDir = cwd app.UpdateStatus() diff --git a/commands/choose.go b/commands/choose.go index 6810ed1f..4e2007fd 100644 --- a/commands/choose.go +++ b/commands/choose.go @@ -6,7 +6,9 @@ import ( "git.sr.ht/~rjarry/aerc/app" ) -type Choose struct{} +type Choose struct { + Unused struct{} `opt:"-"` +} func init() { register(Choose{}) diff --git a/commands/commands.go b/commands/commands.go index 57f2e6dc..c2137a58 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -3,6 +3,7 @@ package commands import ( "bytes" "errors" + "reflect" "sort" "strings" "unicode" @@ -117,7 +118,12 @@ func ExecuteCommand(cmd Command, cmdline string) error { return errors.New("No arguments") } log.Tracef("executing command %s", args.String()) - return cmd.Execute(args.Args()) + // copy zeroed struct + tmp := reflect.New(reflect.TypeOf(cmd)).Interface().(Command) + if err := opt.ArgsToStruct(args.Clone(), tmp); err != nil { + return err + } + return tmp.Execute(args.Args()) } // expand template expressions diff --git a/commands/compose/abort.go b/commands/compose/abort.go index a11c06c5..4bacb9c3 100644 --- a/commands/compose/abort.go +++ b/commands/compose/abort.go @@ -1,8 +1,6 @@ package compose import ( - "errors" - "git.sr.ht/~rjarry/aerc/app" ) @@ -21,12 +19,7 @@ func (Abort) Complete(args []string) []string { } func (Abort) Execute(args []string) error { - if len(args) != 1 { - return errors.New("Usage: abort") - } composer, _ := app.SelectedTabContent().(*app.Composer) - app.RemoveTab(composer, true) - return nil } diff --git a/commands/compose/attach-key.go b/commands/compose/attach-key.go index 74e05bb7..e81a6a5a 100644 --- a/commands/compose/attach-key.go +++ b/commands/compose/attach-key.go @@ -1,8 +1,6 @@ package compose import ( - "errors" - "git.sr.ht/~rjarry/aerc/app" ) @@ -21,11 +19,6 @@ func (AttachKey) Complete(args []string) []string { } func (AttachKey) Execute(args []string) error { - if len(args) != 1 { - return errors.New("Usage: attach-key") - } - composer, _ := app.SelectedTabContent().(*app.Composer) - return composer.SetAttachKey(!composer.AttachKey()) } diff --git a/commands/compose/attach.go b/commands/compose/attach.go index fb533941..3acf28c2 100644 --- a/commands/compose/attach.go +++ b/commands/compose/attach.go @@ -18,11 +18,13 @@ import ( "git.sr.ht/~rjarry/aerc/lib/xdg" "git.sr.ht/~rjarry/aerc/log" "github.com/pkg/errors" - - "git.sr.ht/~sircmpwn/getopt" ) -type Attach struct{} +type Attach struct { + Menu bool `opt:"-m"` + Name string `opt:"-r"` + Path string `opt:"..." metavar:"<path>" required:"false"` +} func init() { register(Attach{}) @@ -38,48 +40,20 @@ func (Attach) Complete(args []string) []string { } func (a Attach) Execute(args []string) error { - var ( - menu bool - read bool - ) - - opts, optind, err := getopt.Getopts(args, "mr") - if err != nil { - return err - } - - for _, opt := range opts { - switch opt.Option { - case 'm': - if read { - return errors.New("-m and -r are mutually exclusive") - } - menu = true - case 'r': - if menu { - return errors.New("-m and -r are mutually exclusive") - } - read = true + if a.Menu && a.Name != "" { + return errors.New("-m and -r are mutually exclusive") + } + switch { + case a.Menu: + return a.openMenu() + case a.Name != "": + if a.Path == "" { + return errors.New("command is required") } + return a.readCommand() + default: + return a.addPath(a.Path) } - - args = args[optind:] - - if menu { - return a.openMenu(args) - } - - if read { - if len(args) < 2 { - return fmt.Errorf("Usage: :attach -r <name> <cmd> [args...]") - } - return a.readCommand(args[0], args[1:]) - } - - if len(args) == 0 { - return fmt.Errorf("Usage: :attach <path>") - } - return a.addPath(strings.Join(args, " ")) } func (a Attach) addPath(path string) error { @@ -129,18 +103,14 @@ func (a Attach) addPath(path string) error { return nil } -func (a Attach) openMenu(args []string) error { +func (a Attach) openMenu() error { filePickerCmd := config.Compose.FilePickerCmd if filePickerCmd == "" { return fmt.Errorf("no file-picker-cmd defined") } if strings.Contains(filePickerCmd, "%s") { - verb := "" - if len(args) > 0 { - verb = args[0] - } - filePickerCmd = strings.ReplaceAll(filePickerCmd, "%s", verb) + filePickerCmd = strings.ReplaceAll(filePickerCmd, "%s", a.Path) } picks, err := os.CreateTemp("", "aerc-filepicker-*") @@ -215,9 +185,8 @@ func (a Attach) openMenu(args []string) error { return nil } -func (a Attach) readCommand(name string, args []string) error { - args = append([]string{"-c"}, args...) - cmd := exec.Command("sh", args...) +func (a Attach) readCommand() error { + cmd := exec.Command("sh", "-c", a.Path) data, err := cmd.Output() if err != nil { @@ -226,20 +195,20 @@ func (a Attach) readCommand(name string, args []string) error { reader := bufio.NewReader(bytes.NewReader(data)) - mimeType, mimeParams, err := lib.FindMimeType(name, reader) + mimeType, mimeParams, err := lib.FindMimeType(a.Name, reader) if err != nil { return errors.Wrap(err, "FindMimeType") } - mimeParams["name"] = name + mimeParams["name"] = a.Name composer, _ := app.SelectedTabContent().(*app.Composer) - err = composer.AddPartAttachment(name, mimeType, mimeParams, reader) + err = composer.AddPartAttachment(a.Name, mimeType, mimeParams, reader) if err != nil { return errors.Wrap(err, "AddPartAttachment") } - app.PushSuccess(fmt.Sprintf("Attached %s", name)) + app.PushSuccess(fmt.Sprintf("Attached %s", a.Name)) return nil } diff --git a/commands/compose/cc-bcc.go b/commands/compose/cc-bcc.go index e1f94c1e..aeb6af97 100644 --- a/commands/compose/cc-bcc.go +++ b/commands/compose/cc-bcc.go @@ -1,12 +1,12 @@ package compose import ( - "strings" - "git.sr.ht/~rjarry/aerc/app" ) -type CC struct{} +type CC struct { + Recipients string `opt:"recipients"` +} func init() { register(CC{}) @@ -20,18 +20,14 @@ func (CC) Complete(args []string) []string { return nil } -func (CC) Execute(args []string) error { - var addrs string - if len(args) > 1 { - addrs = strings.Join(args[1:], " ") - } +func (c CC) Execute(args []string) error { composer, _ := app.SelectedTabContent().(*app.Composer) switch args[0] { case "cc": - return composer.AddEditor("Cc", addrs, true) + return composer.AddEditor("Cc", c.Recipients, true) case "bcc": - return composer.AddEditor("Bcc", addrs, true) + return composer.AddEditor("Bcc", c.Recipients, true) } return nil diff --git a/commands/compose/detach.go b/commands/compose/detach.go index 4847713f..a2996516 100644 --- a/commands/compose/detach.go +++ b/commands/compose/detach.go @@ -2,12 +2,13 @@ package compose import ( "fmt" - "strings" "git.sr.ht/~rjarry/aerc/app" ) -type Detach struct{} +type Detach struct { + Path string `opt:"path" required:"false"` +} func init() { register(Detach{}) @@ -22,27 +23,24 @@ func (Detach) Complete(args []string) []string { return composer.GetAttachments() } -func (Detach) Execute(args []string) error { - var path string +func (d Detach) Execute(args []string) error { composer, _ := app.SelectedTabContent().(*app.Composer) - if len(args) > 1 { - path = strings.Join(args[1:], " ") - } else { + if d.Path == "" { // if no attachment is specified, delete the first in the list atts := composer.GetAttachments() if len(atts) > 0 { - path = atts[0] + d.Path = atts[0] } else { return fmt.Errorf("No attachments to delete") } } - if err := composer.DeleteAttachment(path); err != nil { + if err := composer.DeleteAttachment(d.Path); err != nil { return err } - app.PushSuccess(fmt.Sprintf("Detached %s", path)) + app.PushSuccess(fmt.Sprintf("Detached %s", d.Path)) return nil } diff --git a/commands/compose/edit.go b/commands/compose/edit.go index 2948e964..80f4e6f4 100644 --- a/commands/compose/edit.go +++ b/commands/compose/edit.go @@ -5,10 +5,12 @@ import ( "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/config" - "git.sr.ht/~sircmpwn/getopt" ) -type Edit struct{} +type Edit struct { + Edit bool `opt:"-e"` + NoEdit bool `opt:"-E"` +} func init() { register(Edit{}) @@ -22,30 +24,15 @@ func (Edit) Complete(args []string) []string { return nil } -func (Edit) Execute(args []string) error { +func (e Edit) Execute(args []string) error { composer, ok := app.SelectedTabContent().(*app.Composer) if !ok { return errors.New("only valid while composing") } - editHeaders := config.Compose.EditHeaders - opts, optind, err := getopt.Getopts(args, "eE") - if err != nil { - return err - } - if len(args) != optind { - return errors.New("Usage: edit [-e|-E]") - } - for _, opt := range opts { - switch opt.Option { - case 'e': - editHeaders = true - case 'E': - editHeaders = false - } - } + editHeaders := (config.Compose.EditHeaders || e.Edit) && !e.NoEdit - err = composer.ShowTerminal(editHeaders) + err := composer.ShowTerminal(editHeaders) if err != nil { return err } diff --git a/commands/compose/encrypt.go b/commands/compose/encrypt.go index b9094c5e..3121dff0 100644 --- a/commands/compose/encrypt.go +++ b/commands/compose/encrypt.go @@ -1,8 +1,6 @@ package compose import ( - "errors" - "git.sr.ht/~rjarry/aerc/app" ) @@ -21,12 +19,7 @@ func (Encrypt) Complete(args []string) []string { } func (Encrypt) Execute(args []string) error { - if len(args) != 1 { - return errors.New("Usage: encrypt") - } - composer, _ := app.SelectedTabContent().(*app.Composer) - composer.SetEncrypt(!composer.Encrypt()) return nil } diff --git a/commands/compose/header.go b/commands/compose/header.go index 5c13fde5..aaa14e43 100644 --- a/commands/compose/header.go +++ b/commands/compose/header.go @@ -6,10 +6,14 @@ import ( "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/commands" - "git.sr.ht/~sircmpwn/getopt" ) -type Header struct{} +type Header struct { + Force bool `opt:"-f"` + Remove bool `opt:"-d"` + Name string `opt:"name"` + Value string `opt:"..." required:"false"` +} var headers = []string{ "From", @@ -38,47 +42,25 @@ func (Header) Complete(args []string) []string { } func (h Header) Execute(args []string) error { - opts, optind, err := getopt.Getopts(args, h.Options()) - args = args[optind:] - if err == nil && len(args) < 1 { - err = fmt.Errorf("not enough arguments") - } - if err != nil { - return fmt.Errorf("%w. usage: header [-fd] <name> [<value>]", err) - } - - var force bool = false - var remove bool = false - for _, opt := range opts { - switch opt.Option { - case 'f': - force = true - case 'd': - remove = true - } - } - composer, _ := app.SelectedTabContent().(*app.Composer) - name := strings.TrimRight(args[0], ":") + name := strings.TrimRight(h.Name, ":") - if remove { + if h.Remove { return composer.DelEditor(name) } - value := strings.Join(args[1:], " ") - - if !force { + if !h.Force { headers, err := composer.PrepareHeader() if err != nil { return err } - if headers.Get(name) != "" && value != "" { + if headers.Get(name) != "" && h.Value != "" { return fmt.Errorf( "Header %s is already set to %q (use -f to overwrite)", name, headers.Get(name)) } } - return composer.AddEditor(name, value, false) + return composer.AddEditor(name, h.Value, false) } diff --git a/commands/compose/multipart.go b/commands/compose/multipart.go index 0ad1dc4d..96941062 100644 --- a/commands/compose/multipart.go +++ b/commands/compose/multipart.go @@ -7,10 +7,12 @@ import ( "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/commands" "git.sr.ht/~rjarry/aerc/config" - "git.sr.ht/~sircmpwn/getopt" ) -type Multipart struct{} +type Multipart struct { + Remove bool `opt:"-d"` + Mime string `opt:"mime" metavar:"<mime/type>"` +} func init() { register(Multipart{}) @@ -29,37 +31,21 @@ func (Multipart) Complete(args []string) []string { return commands.CompletionFromList(completions, args) } -func (a Multipart) Execute(args []string) error { +func (m Multipart) Execute(args []string) error { composer, ok := app.SelectedTabContent().(*app.Composer) if !ok { return fmt.Errorf(":multipart is only available on the compose::review screen") } - opts, optind, err := getopt.Getopts(args, "d") - if err != nil { - return fmt.Errorf("Usage: :multipart [-d] <mime/type>") - } - var remove bool = false - for _, opt := range opts { - if opt.Option == 'd' { - remove = true - } - } - args = args[optind:] - if len(args) != 1 { - return fmt.Errorf("Usage: :multipart [-d] <mime/type>") - } - mime := args[0] - - if remove { - return composer.RemovePart(mime) + if m.Remove { + return composer.RemovePart(m.Mime) } else { - _, found := config.Converters[mime] + _, found := config.Converters[m.Mime] if !found { - return fmt.Errorf("no command defined for MIME type: %s", mime) + return fmt.Errorf("no command defined for MIME type: %s", m.Mime) } - err = composer.AppendPart( - mime, + err := composer.AppendPart( + m.Mime, map[string]string{"Charset": "UTF-8"}, // the actual content of the part will be rendered // every time the body of the email is updated diff --git a/commands/compose/next-field.go b/commands/compose/next-field.go index be5f8e53..88fbb03d 100644 --- a/commands/compose/next-field.go +++ b/commands/compose/next-field.go @@ -1,8 +1,6 @@ package compose import ( - "fmt" - "git.sr.ht/~rjarry/aerc/app" ) @@ -21,9 +19,6 @@ func (NextPrevField) Complete(args []string) []string { } func (NextPrevField) Execute(args []string) error { - if len(args) > 2 { - return nextPrevFieldUsage(args[0]) - } composer, _ := app.SelectedTabContent().(*app.Composer) if args[0] == "prev-field" { composer.PrevField() @@ -32,7 +27,3 @@ func (NextPrevField) Execute(args []string) error { } return nil } - -func nextPrevFieldUsage(cmd string) error { - return fmt.Errorf("Usage: %s", cmd) -} diff --git a/commands/compose/postpone.go b/commands/compose/postpone.go index ac16d904..767e759c 100644 --- a/commands/compose/postpone.go +++ b/commands/compose/postpone.go @@ -6,8 +6,6 @@ import ( "github.com/pkg/errors" - "git.sr.ht/~sircmpwn/getopt" - "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/commands" "git.sr.ht/~rjarry/aerc/log" @@ -15,7 +13,9 @@ import ( "git.sr.ht/~rjarry/aerc/worker/types" ) -type Postpone struct{} +type Postpone struct { + Folder string `opt:"-t"` +} func init() { register(Postpone{}) @@ -42,11 +42,6 @@ func (Postpone) Complete(args []string) []string { } func (p Postpone) Execute(args []string) error { - opts, optind, err := getopt.Getopts(args, p.Options()) - if err != nil { - return err - } - acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") @@ -63,15 +58,8 @@ func (p Postpone) Execute(args []string) error { if composer.RecalledFrom() != "" { targetFolder = composer.RecalledFrom() } - for _, opt := range opts { - if opt.Option == 't' { - targetFolder = opt.Value - } - } - args = args[optind:] - - if len(args) != 0 { - return errors.New("Usage: postpone [-t <folder>]") + if p.Folder != "" { + targetFolder = p.Folder } if targetFolder == "" { return errors.New("No Postpone location configured") diff --git a/commands/compose/send.go b/commands/compose/send.go index cd964d70..e3672471 100644 --- a/commands/compose/send.go +++ b/commands/compose/send.go @@ -10,7 +10,6 @@ import ( "strings" "time" - "git.sr.ht/~sircmpwn/getopt" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" "github.com/google/shlex" @@ -19,6 +18,7 @@ import ( "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/commands" "git.sr.ht/~rjarry/aerc/commands/mode" + "git.sr.ht/~rjarry/aerc/commands/msg" "git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" @@ -27,7 +27,10 @@ import ( "golang.org/x/oauth2" ) -type Send struct{} +type Send struct { + Archive string `opt:"-a" action:"ParseArchive" metavar:"flat|year|month"` + CopyTo string `opt:"-t"` +} func init() { register(Send{}) @@ -52,14 +55,17 @@ func (Send) Complete(args []string) []string { return nil } -func (s Send) Execute(args []string) error { - opts, optind, err := getopt.Getopts(args, s.Options()) - if err != nil { - return err - } - if optind != len(args) { - return errors.New("Usage: send [-a <flat|year|month>] [-t <folder>") +func (s *Send) ParseArchive(arg string) error { + for _, a := range msg.ARCHIVE_TYPES { + if a == arg { + s.Archive = arg + return nil + } } + return errors.New("unsupported archive type") +} + +func (s Send) Execute(args []string) error { tab := app.SelectedTab() if tab == nil { return errors.New("No selected tab") @@ -69,15 +75,8 @@ func (s Send) Execute(args []string) error { config := composer.Config() - var copyto string = config.CopyTo - var archive string - for _, opt := range opts { - if opt.Option == 'a' { - archive = opt.Value - } - if opt.Option == 't' { - copyto = opt.Value - } + if s.CopyTo == "" { + s.CopyTo = config.CopyTo } outgoing, err := config.Outgoing.ConnectionString() @@ -115,13 +114,14 @@ func (s Send) Execute(args []string) error { domain = domain_ } ctx := sendCtx{ - uri: uri, - scheme: scheme, - auth: auth, - from: config.From, - rcpts: rcpts, - domain: domain, - copyto: copyto, + uri: uri, + scheme: scheme, + auth: auth, + from: config.From, + rcpts: rcpts, + domain: domain, + archive: s.Archive, + copyto: s.CopyTo, } log.Debugf("send config uri: %s", ctx.uri) @@ -148,7 +148,7 @@ func (s Send) Execute(args []string) error { msg+" Abort send? [Y/n] ", func(text string) { if text == "n" || text == "N" { - send(composer, ctx, header, tabName, archive) + send(composer, ctx, header, tabName) } }, func(cmd string) ([]string, string) { if cmd == "" { @@ -161,14 +161,14 @@ func (s Send) Execute(args []string) error { app.PushPrompt(prompt) } else { - send(composer, ctx, header, tabName, archive) + send(composer, ctx, header, tabName) } return nil } func send(composer *app.Composer, ctx sendCtx, - header *mail.Header, tabName string, archive string, + header *mail.Header, tabName string, ) { // we don't want to block the UI thread while we are sending // so we do everything in a goroutine and hide the composer from the user @@ -243,13 +243,13 @@ func send(composer *app.Composer, ctx sendCtx, "message sent, but copying to %v failed: %v", ctx.copyto, err.Error()) app.PushError(errmsg) - composer.SetSent(archive) + composer.SetSent(ctx.archive) composer.Close() return } } app.PushStatus("Message sent.", 10*time.Second) - composer.SetSent(archive) + composer.SetSent(ctx.archive) composer.Close() }() } @@ -267,13 +267,14 @@ func listRecipients(h *mail.Header) ([]*mail.Address, error) { } type sendCtx struct { - uri *url.URL - scheme string - auth string - from *mail.Address - rcpts []*mail.Address - domain string - copyto string + uri *url.URL + scheme string + auth string + from *mail.Address + rcpts []*mail.Address + domain string + copyto string + archive string } func newSendmailSender(ctx sendCtx) (io.WriteCloser, error) { diff --git a/commands/compose/sign.go b/commands/compose/sign.go index ae7c3417..e30fca05 100644 --- a/commands/compose/sign.go +++ b/commands/compose/sign.go @@ -1,7 +1,6 @@ package compose import ( - "errors" "time" "git.sr.ht/~rjarry/aerc/app" @@ -22,10 +21,6 @@ func (Sign) Complete(args []string) []string { } func (Sign) Execute(args []string) error { - if len(args) != 1 { - return errors.New("Usage: sign") - } - composer, _ := app.SelectedTabContent().(*app.Composer) err := composer.SetSign(!composer.Sign()) diff --git a/commands/compose/switch.go b/commands/compose/switch.go index 1cc388f1..0c027a41 100644 --- a/commands/compose/switch.go +++ b/commands/compose/switch.go @@ -2,17 +2,19 @@ package compose import ( "errors" - "fmt" "git.sr.ht/~rjarry/aerc/app" - "git.sr.ht/~sircmpwn/getopt" ) type AccountSwitcher interface { SwitchAccount(*app.AccountView) error } -type SwitchAccount struct{} +type SwitchAccount struct { + Next bool `opt:"-n"` + Prev bool `opt:"-p"` + Account string `opt:"..." metavar:"<account>" required:"false"` +} func init() { register(SwitchAccount{}) @@ -26,30 +28,9 @@ func (SwitchAccount) Complete(args []string) []string { return app.AccountNames() } -func (SwitchAccount) Execute(args []string) error { - opts, optind, err := getopt.Getopts(args, "np") - if err != nil { - return err - } - var next, prev bool - for _, opt := range opts { - switch opt.Option { - case 'n': - next = true - prev = false - case 'p': - next = false - prev = true - } - } - posargs := args[optind:] - // NOT ((prev || next) XOR (len(posargs) == 1)) - if (prev || next) == (len(posargs) == 1) { - name := "" - if acct := app.SelectedAccount(); acct != nil { - name = fmt.Sprintf("Current account: %s. ", acct.Name()) - } - return errors.New(name + "Usage: switch-account [-np] <account-name>") +func (s SwitchAccount) Execute(args []string) error { + if !s.Prev && !s.Next && s.Account == "" { + return errors.New("Usage: switch-account -n | -p | <account-name>") } switcher, ok := app.SelectedTabContent().(AccountSwitcher) @@ -58,14 +39,15 @@ func (SwitchAccount) Execute(args []string) error { } var acct *app.AccountView + var err error switch { - case prev: + case s.Prev: acct, err = app.PrevAccount() - case next: + case s.Next: acct, err = app.NextAccount() default: - acct, err = app.Account(posargs[0]) + acct, err = app.Account(s.Account) } if err != nil { return err diff --git a/commands/ct.go b/commands/ct.go index 1b5659c7..8a6bb063 100644 --- a/commands/ct.go +++ b/commands/ct.go @@ -2,14 +2,15 @@ package commands import ( "errors" - "fmt" "strconv" "strings" "git.sr.ht/~rjarry/aerc/app" ) -type ChangeTab struct{} +type ChangeTab struct { + Tab string `opt:"tab"` +} func init() { register(ChangeTab{}) @@ -27,25 +28,21 @@ func (ChangeTab) Complete(args []string) []string { return FilterList(app.TabNames(), joinedArgs, "", app.SelectedAccountUiConfig().FuzzyComplete) } -func (ChangeTab) Execute(args []string) error { - if len(args) == 1 { - return fmt.Errorf("Usage: %s <tab>", args[0]) - } - joinedArgs := strings.Join(args[1:], " ") - if joinedArgs == "-" { +func (c ChangeTab) Execute(args []string) error { + if c.Tab == "-" { ok := app.SelectPreviousTab() if !ok { return errors.New("No previous tab to return to") } } else { - n, err := strconv.Atoi(joinedArgs) + n, err := strconv.Atoi(c.Tab) if err == nil { switch { - case strings.HasPrefix(joinedArgs, "+"): + case strings.HasPrefix(c.Tab, "+"): for ; n > 0; n-- { app.NextTab() } - case strings.HasPrefix(joinedArgs, "-"): + case strings.HasPrefix(c.Tab, "-"): for ; n < 0; n++ { app.PrevTab() } @@ -57,7 +54,7 @@ func (ChangeTab) Execute(args []string) error { } } } else { - ok := app.SelectTab(joinedArgs) + ok := app.SelectTab(c.Tab) if !ok { return errors.New("No tab with that name") } diff --git a/commands/eml.go b/commands/eml.go index e03792ef..adacd05b 100644 --- a/commands/eml.go +++ b/commands/eml.go @@ -11,7 +11,9 @@ import ( "git.sr.ht/~rjarry/aerc/lib" ) -type Eml struct{} +type Eml struct { + Path string `opt:"path" required:"false"` +} func init() { register(Eml{}) @@ -25,7 +27,7 @@ func (Eml) Complete(args []string) []string { return CompletePath(strings.Join(args, " ")) } -func (Eml) Execute(args []string) error { +func (e Eml) Execute(args []string) error { acct := app.SelectedAccount() if acct == nil { return fmt.Errorf("no account selected") @@ -49,7 +51,7 @@ func (Eml) Execute(args []string) error { }) } - if len(args) == 1 { + if e.Path == "" { switch tab := app.SelectedTabContent().(type) { case *app.MessageViewer: part := tab.SelectedMessagePart() @@ -68,11 +70,7 @@ func (Eml) Execute(args []string) error { return fmt.Errorf("unsupported operation") } } else { - path := strings.Join(args[1:], " ") - if _, err := os.Stat(path); err != nil { - return err - } - f, err := os.Open(path) + f, err := os.Open(e.Path) if err != nil { return err } diff --git a/commands/exec.go b/commands/exec.go index a34f54e7..4c2f3d1b 100644 --- a/commands/exec.go +++ b/commands/exec.go @@ -1,7 +1,6 @@ package commands import ( - "errors" "fmt" "os" "os/exec" @@ -11,7 +10,9 @@ import ( "git.sr.ht/~rjarry/aerc/log" ) -type ExecCmd struct{} +type ExecCmd struct { + Args []string `opt:"..."` +} func init() { register(ExecCmd{}) @@ -25,12 +26,8 @@ func (ExecCmd) Complete(args []string) []string { return nil } -func (ExecCmd) Execute(args []string) error { - if len(args) < 2 { - return errors.New("Usage: exec [cmd...]") - } - - cmd := exec.Command(args[1], args[2:]...) +func (e ExecCmd) Execute(args []string) error { + cmd := exec.Command(e.Args[0], e.Args[1:]...) env := os.Environ() switch view := app.SelectedTabContent().(type) { diff --git a/commands/help.go b/commands/help.go index 5cc0aacc..b2bcdf7c 100644 --- a/commands/help.go +++ b/commands/help.go @@ -1,12 +1,14 @@ package commands import ( - "errors" + "fmt" "git.sr.ht/~rjarry/aerc/app" ) -type Help struct{} +type Help struct { + Topic string `opt:"topic" action:"ParseTopic" default:"aerc"` +} var pages = []string{ "aerc", @@ -37,15 +39,21 @@ func (Help) Complete(args []string) []string { return CompletionFromList(pages, args) } -func (Help) Execute(args []string) error { - page := "aerc" - if len(args) == 2 && args[1] != "aerc" { - page = "aerc-" + args[1] - } else if len(args) > 2 { - return errors.New("Usage: help [topic]") +func (h *Help) ParseTopic(arg string) error { + for _, page := range pages { + if arg == page { + if arg != "aerc" { + arg = "aerc-" + arg + } + h.Topic = arg + return nil + } } + return fmt.Errorf("unknown topic %q", arg) +} - if page == "aerc-keys" { +func (h Help) Execute(args []string) error { + if h.Topic == "aerc-keys" { app.AddDialog(app.NewDialog( app.NewListBox( "Bindings: Press <Esc> or <Enter> to close. "+ @@ -61,6 +69,6 @@ func (Help) Execute(args []string) error { )) return nil } - - return TermCore([]string{"term", "man", page}) + term := Term{Cmd: []string{"man", h.Topic}} + return term.Execute(args) } diff --git a/commands/move-tab.go b/commands/move-tab.go index 1aa20880..23580f2d 100644 --- a/commands/move-tab.go +++ b/commands/move-tab.go @@ -1,19 +1,33 @@ package commands import ( - "fmt" "strconv" "strings" "git.sr.ht/~rjarry/aerc/app" ) -type MoveTab struct{} +type MoveTab struct { + Index int `opt:"index" metavar:"[+|-]<index>" action:"ParseIndex"` + Relative bool +} func init() { register(MoveTab{}) } +func (m *MoveTab) ParseIndex(arg string) error { + i, err := strconv.ParseInt(arg, 10, 64) + if err != nil { + return err + } + m.Index = int(i) + if strings.HasPrefix(arg, "+") || strings.HasPrefix(arg, "-") { + m.Relative = true + } + return nil +} + func (MoveTab) Aliases() []string { return []string{"move-tab"} } @@ -22,23 +36,7 @@ func (MoveTab) Complete(args []string) []string { return nil } -func (MoveTab) Execute(args []string) error { - if len(args) == 1 { - return fmt.Errorf("Usage: %s [+|-]<index>", args[0]) - } - - joinedArgs := strings.Join(args[1:], "") - - n, err := strconv.Atoi(joinedArgs) - if err != nil { - return fmt.Errorf("failed to parse index argument: %w", err) - } - - var relative bool - if strings.HasPrefix(joinedArgs, "+") || strings.HasPrefix(joinedArgs, "-") { - relative = true - } - app.MoveTab(n, relative) - +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 f326d0c6..f4d6e3be 100644 --- a/commands/msg/archive.go +++ b/commands/msg/archive.go @@ -1,7 +1,6 @@ package msg import ( - "errors" "fmt" "strings" "sync" @@ -19,7 +18,21 @@ const ( ARCHIVE_MONTH = "month" ) -type Archive struct{} +var ARCHIVE_TYPES = []string{ARCHIVE_FLAT, ARCHIVE_YEAR, ARCHIVE_MONTH} + +type Archive struct { + Type string `opt:"type" action:"ParseArchiveType" metavar:"flat|year|month"` +} + +func (a *Archive) ParseArchiveType(arg string) error { + for _, t := range ARCHIVE_TYPES { + if t == arg { + a.Type = arg + return nil + } + } + return fmt.Errorf("invalid archive type") +} func init() { register(Archive{}) @@ -34,16 +47,13 @@ func (Archive) Complete(args []string) []string { return commands.CompletionFromList(valid, args) } -func (Archive) Execute(args []string) error { - if len(args) != 2 { - return errors.New("Usage: archive <flat|year|month>") - } +func (a Archive) Execute(args []string) error { h := newHelper() msgs, err := h.messages() if err != nil { return err } - err = archive(msgs, args[1]) + err = archive(msgs, a.Type) return err } diff --git a/commands/msg/copy.go b/commands/msg/copy.go index 1a902772..4109ef99 100644 --- a/commands/msg/copy.go +++ b/commands/msg/copy.go @@ -1,18 +1,17 @@ package msg import ( - "errors" - "strings" "time" - "git.sr.ht/~sircmpwn/getopt" - "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/commands" "git.sr.ht/~rjarry/aerc/worker/types" ) -type Copy struct{} +type Copy struct { + CreateFolders bool `opt:"-p"` + Folder string `opt:"..." metavar:"<folder>"` +} func init() { register(Copy{}) @@ -26,20 +25,7 @@ func (Copy) Complete(args []string) []string { return commands.GetFolders(args) } -func (Copy) Execute(args []string) error { - if len(args) == 1 { - return errors.New("Usage: cp [-p] <folder>") - } - opts, optind, err := getopt.Getopts(args, "p") - if err != nil { - return err - } - var createParents bool - for _, opt := range opts { - if opt.Option == 'p' { - createParents = true - } - } +func (c Copy) Execute(args []string) error { h := newHelper() uids, err := h.markedOrSelectedUids() if err != nil { @@ -49,8 +35,8 @@ func (Copy) Execute(args []string) error { if err != nil { return err } - store.Copy(uids, strings.Join(args[optind:], " "), - createParents, func( + store.Copy(uids, c.Folder, + c.CreateFolders, func( msg types.WorkerMessage, ) { switch msg := msg.(type) { diff --git a/commands/msg/delete.go b/commands/msg/delete.go index 107c8a3f..49463abc 100644 --- a/commands/msg/delete.go +++ b/commands/msg/delete.go @@ -1,7 +1,6 @@ package msg import ( - "errors" "time" "git.sr.ht/~rjarry/aerc/app" @@ -27,10 +26,6 @@ func (Delete) Complete(args []string) []string { } func (Delete) Execute(args []string) error { - if len(args) != 1 { - return errors.New("Usage: :delete") - } - h := newHelper() store, err := h.store() if err != nil { diff --git a/commands/msg/envelope.go b/commands/msg/envelope.go index 3a388c12..6da82a1e 100644 --- a/commands/msg/envelope.go +++ b/commands/msg/envelope.go @@ -9,11 +9,13 @@ import ( "git.sr.ht/~rjarry/aerc/lib/format" "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" - "git.sr.ht/~sircmpwn/getopt" "github.com/emersion/go-message/mail" ) -type Envelope struct{} +type Envelope struct { + Header bool `opt:"-h"` + Format string `opt:"-s" default:"%-20.20s: %s"` +} func init() { register(Envelope{}) @@ -27,22 +29,7 @@ func (Envelope) Complete(args []string) []string { return nil } -func (Envelope) Execute(args []string) error { - header := false - fmtStr := "%-20.20s: %s" - opts, _, err := getopt.Getopts(args, "hs:") - if err != nil { - return err - } - for _, opt := range opts { - switch opt.Option { - case 's': - fmtStr = opt.Value - case 'h': - header = true - } - } - +func (e Envelope) Execute(args []string) error { acct := app.SelectedAccount() if acct == nil { return errors.New("No account selected") @@ -53,10 +40,10 @@ func (Envelope) Execute(args []string) error { return err } else { if msg != nil { - if header { - list = parseHeader(msg, fmtStr) + if e.Header { + list = parseHeader(msg, e.Format) } else { - list = parseEnvelope(msg, fmtStr, + list = parseEnvelope(msg, e.Format, acct.UiConfig().TimestampFormat) } } else { diff --git a/commands/msg/fold.go b/commands/msg/fold.go index 1d40b90a..0621c8c3 100644 --- a/commands/msg/fold.go +++ b/commands/msg/fold.go @@ -2,7 +2,6 @@ package msg import ( "errors" - "fmt" "strings" "git.sr.ht/~rjarry/aerc/lib/ui" @@ -23,9 +22,6 @@ func (Fold) Complete(args []string) []string { } func (Fold) Execute(args []string) error { - if len(args) != 1 { - return fmt.Errorf("Usage: %s", args[0]) - } h := newHelper() store, err := h.store() if err != nil { diff --git a/commands/msg/forward.go b/commands/msg/forward.go index 68f162cc..e6e386a9 100644 --- a/commands/msg/forward.go +++ b/commands/msg/forward.go @@ -20,11 +20,16 @@ import ( "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/worker/types" "github.com/emersion/go-message/mail" - - "git.sr.ht/~sircmpwn/getopt" ) -type forward struct{} +type forward struct { + AttachAll bool `opt:"-A"` + AttachFull bool `opt:"-F"` + Edit bool `opt:"-e"` + NoEdit bool `opt:"-E"` + Template string `opt:"-T"` + To []string `opt:"..." required:"false"` +} func init() { register(forward{}) @@ -38,36 +43,11 @@ func (forward) Complete(args []string) []string { return nil } -func (forward) Execute(args []string) error { - opts, optind, err := getopt.Getopts(args, "AFT:eE") - if err != nil { - return err - } - if len(args) != optind { - return errors.New("Usage: forward [-A|-F] [-T <template>] [-e|-E]") - } - attachAll := false - attachFull := false - template := "" - editHeaders := config.Compose.EditHeaders - for _, opt := range opts { - switch opt.Option { - case 'A': - attachAll = true - case 'F': - attachFull = true - case 'T': - template = opt.Value - case 'e': - editHeaders = true - case 'E': - editHeaders = false - } - } - - if attachAll && attachFull { +func (f forward) Execute(args []string) error { + if f.AttachAll && f.AttachFull { return errors.New("Options -A and -F are mutually exclusive") } + editHeaders := (config.Compose.EditHeaders || f.Edit) && !f.NoEdit widget := app.SelectedTabContent().(app.ProvidesMessage) acct := widget.SelectedAccount() @@ -89,7 +69,7 @@ func (forward) Execute(args []string) error { h.SetSubject(subject) var tolist []*mail.Address - to := strings.Join(args[optind:], ", ") + to := strings.Join(f.To, ", ") if strings.Contains(to, "@") { tolist, err = mail.ParseAddressList(to) if err != nil { @@ -109,7 +89,7 @@ func (forward) Execute(args []string) error { addTab := func() (*app.Composer, error) { composer, err := app.NewComposer(acct, acct.AccountConfig(), acct.Worker(), editHeaders, - template, h, &original, nil) + f.Template, h, &original, nil) if err != nil { app.PushError("Error: " + err.Error()) return nil, err @@ -124,7 +104,7 @@ func (forward) Execute(args []string) error { return composer, nil } - if attachFull { + if f.AttachFull { tmpDir, err := os.MkdirTemp("", "aerc-tmp-attachment") if err != nil { return err @@ -158,8 +138,8 @@ func (forward) Execute(args []string) error { }) }) } else { - if template == "" { - template = config.Templates.Forwards + if f.Template == "" { + f.Template = config.Templates.Forwards } part := lib.FindPlaintext(msg.BodyStructure, nil) @@ -186,7 +166,7 @@ func (forward) Execute(args []string) error { } // add attachments - if attachAll { + if f.AttachAll { var mu sync.Mutex parts := lib.FindAllNonMultipart(msg.BodyStructure, nil, nil) for _, p := range parts { diff --git a/commands/msg/invite.go b/commands/msg/invite.go index 60107480..5b8558b0 100644 --- a/commands/msg/invite.go +++ b/commands/msg/invite.go @@ -12,11 +12,13 @@ import ( "git.sr.ht/~rjarry/aerc/lib/format" "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" - "git.sr.ht/~sircmpwn/getopt" "github.com/emersion/go-message/mail" ) -type invite struct{} +type invite struct { + Edit bool `opt:"-e"` + NoEdit bool `opt:"-E"` +} func init() { register(invite{}) @@ -30,7 +32,7 @@ func (invite) Complete(args []string) []string { return nil } -func (invite) Execute(args []string) error { +func (i invite) Execute(args []string) error { acct := app.SelectedAccount() if acct == nil { return errors.New("no account selected") @@ -49,22 +51,7 @@ func (invite) Execute(args []string) error { return fmt.Errorf("no invitation found (missing text/calendar)") } - editHeaders := config.Compose.EditHeaders - opts, optind, err := getopt.Getopts(args, "eE") - if err != nil { - return err - } - if len(args) != optind { - return errors.New("Usage: accept|accept-tentative|decline [-e|-E]") - } - for _, opt := range opts { - switch opt.Option { - case 'e': - editHeaders = true - case 'E': - editHeaders = false - } - } + editHeaders := (config.Compose.EditHeaders || i.Edit) && !i.NoEdit subject := trimLocalizedRe(msg.Envelope.Subject, acct.AccountConfig().LocalizedRe) switch args[0] { diff --git a/commands/msg/mark.go b/commands/msg/mark.go index eeaa5485..c2c21cf2 100644 --- a/commands/msg/mark.go +++ b/commands/msg/mark.go @@ -2,11 +2,15 @@ package msg import ( "fmt" - - "git.sr.ht/~sircmpwn/getopt" ) -type Mark struct{} +type Mark struct { + All bool `opt:"-a" aliases:"mark,unmark"` + Toggle bool `opt:"-t" aliases:"mark,unmark"` + Visual bool `opt:"-v" aliases:"mark,unmark"` + VisualClear bool `opt:"-V" aliases:"mark,unmark"` + Thread bool `opt:"-T" aliases:"mark,unmark"` +} func init() { register(Mark{}) @@ -20,7 +24,7 @@ func (Mark) Complete(args []string) []string { return nil } -func (Mark) Execute(args []string) error { +func (m Mark) Execute(args []string) error { h := newHelper() OnSelectedMessage := func(fn func(uint32)) error { if fn == nil { @@ -38,63 +42,38 @@ func (Mark) Execute(args []string) error { return err } marker := store.Marker() - opts, _, err := getopt.Getopts(args, "atvVT") - if err != nil { - return err - } - var all bool - var toggle bool - var visual bool - var clearVisual bool - var thread bool - for _, opt := range opts { - switch opt.Option { - case 'a': - all = true - case 'v': - visual = true - clearVisual = true - case 'V': - visual = true - case 't': - toggle = true - case 'T': - thread = true - } - } - if thread && all { + if m.Thread && m.All { return fmt.Errorf("-a and -T are mutually exclusive") } - if thread && visual { + if m.Thread && (m.Visual || m.VisualClear) { return fmt.Errorf("-v and -T are mutually exclusive") } + if m.Visual && m.All { + return fmt.Errorf("-a and -v are mutually exclusive") + } switch args[0] { case "mark": - if all && visual { - return fmt.Errorf("-a and -v are mutually exclusive") - } - var modFunc func(uint32) - if toggle { + if m.Toggle { modFunc = marker.ToggleMark } else { modFunc = marker.Mark } switch { - case all: + case m.All: uids := store.Uids() for _, uid := range uids { modFunc(uid) } return nil - case visual: - marker.ToggleVisualMark(clearVisual) + case m.Visual || m.VisualClear: + marker.ToggleVisualMark(m.VisualClear) return nil default: - if thread { + if m.Thread { threadPtr, err := store.SelectedThread() if err != nil { return err @@ -109,22 +88,22 @@ func (Mark) Execute(args []string) error { } case "unmark": - if visual { + if m.Visual || m.VisualClear { return fmt.Errorf("visual mode not supported for this command") } switch { - case all && toggle: + case m.All && m.Toggle: uids := store.Uids() for _, uid := range uids { marker.ToggleMark(uid) } return nil - case all && !toggle: + case m.All && !m.Toggle: marker.ClearVisualMark() return nil default: - if thread { + if m.Thread { threadPtr, err := store.SelectedThread() if err != nil { return err @@ -138,9 +117,6 @@ func (Mark) Execute(args []string) error { return nil } case "remark": - if all || visual || toggle || thread { - return fmt.Errorf("Usage: :remark") - } marker.Remark() return nil } diff --git a/commands/msg/modify-labels.go b/commands/msg/modify-labels.go index d219a57e..6fdbeac4 100644 --- a/commands/msg/modify-labels.go +++ b/commands/msg/modify-labels.go @@ -1,7 +1,6 @@ package msg import ( - "errors" "time" "git.sr.ht/~rjarry/aerc/app" @@ -9,7 +8,9 @@ import ( "git.sr.ht/~rjarry/aerc/worker/types" ) -type ModifyLabels struct{} +type ModifyLabels struct { + Labels []string `opt:"..." metavar:"[+-]<label>"` +} func init() { register(ModifyLabels{}) @@ -23,12 +24,7 @@ func (ModifyLabels) Complete(args []string) []string { return commands.GetLabels(args) } -func (ModifyLabels) Execute(args []string) error { - changes := args[1:] - if len(changes) == 0 { - return errors.New("Usage: modify-labels <[+-]label> ...") - } - +func (m ModifyLabels) Execute(args []string) error { h := newHelper() store, err := h.store() if err != nil { @@ -40,7 +36,7 @@ func (ModifyLabels) Execute(args []string) error { } var add, remove []string - for _, l := range changes { + for _, l := range m.Labels { switch l[0] { case '+': add = append(add, l[1:]) diff --git a/commands/msg/move.go b/commands/msg/move.go index 5ef9390a..1dd68d35 100644 --- a/commands/msg/move.go +++ b/commands/msg/move.go @@ -1,8 +1,6 @@ package msg import ( - "errors" - "strings" "time" "git.sr.ht/~rjarry/aerc/app" @@ -12,10 +10,12 @@ import ( "git.sr.ht/~rjarry/aerc/lib/ui" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/worker/types" - "git.sr.ht/~sircmpwn/getopt" ) -type Move struct{} +type Move struct { + CreateFolders bool `opt:"-p"` + Folder string `opt:"..." metavar:"<folder>"` +} func init() { register(Move{}) @@ -29,21 +29,7 @@ func (Move) Complete(args []string) []string { return commands.GetFolders(args) } -func (Move) Execute(args []string) error { - if len(args) == 1 { - return errors.New("Usage: mv [-p] <folder>") - } - opts, optind, err := getopt.Getopts(args, "p") - if err != nil { - return err - } - var createParents bool - for _, opt := range opts { - if opt.Option == 'p' { - createParents = true - } - } - +func (m Move) Execute(args []string) error { h := newHelper() acct, err := h.account() if err != nil { @@ -64,14 +50,13 @@ func (Move) Execute(args []string) error { marker := store.Marker() marker.ClearVisualMark() next := findNextNonDeleted(uids, store) - joinedArgs := strings.Join(args[optind:], " ") - store.Move(uids, joinedArgs, createParents, func( + store.Move(uids, m.Folder, m.CreateFolders, func( msg types.WorkerMessage, ) { switch msg := msg.(type) { case *types.Done: - handleDone(acct, next, "Messages moved to "+joinedArgs, store) + handleDone(acct, next, "Messages moved to "+m.Folder, store) case *types.Error: app.PushError(msg.Error.Error()) marker.Remark() diff --git a/commands/msg/pipe.go b/commands/msg/pipe.go index c9d88f40..75b63b10 100644 --- a/commands/msg/pipe.go +++ b/commands/msg/pipe.go @@ -14,11 +14,14 @@ import ( "git.sr.ht/~rjarry/aerc/log" mboxer "git.sr.ht/~rjarry/aerc/worker/mbox" "git.sr.ht/~rjarry/aerc/worker/types" - - "git.sr.ht/~sircmpwn/getopt" ) -type Pipe struct{} +type Pipe struct { + Background bool `opt:"-b"` + Full bool `opt:"-m"` + Part bool `opt:"-p"` + Command []string `opt:"..."` +} func init() { register(Pipe{}) @@ -32,44 +35,17 @@ func (Pipe) Complete(args []string) []string { return nil } -func (Pipe) Execute(args []string) error { - var ( - background bool - pipeFull bool - pipePart bool - ) - // TODO: let user specify part by index or preferred mimetype - opts, optind, err := getopt.Getopts(args, "bmp") - if err != nil { - return err - } - for _, opt := range opts { - switch opt.Option { - case 'b': - background = true - case 'm': - if pipePart { - return errors.New("-m and -p are mutually exclusive") - } - pipeFull = true - case 'p': - if pipeFull { - return errors.New("-m and -p are mutually exclusive") - } - pipePart = true - } - } - cmd := args[optind:] - if len(cmd) == 0 { - return errors.New("Usage: pipe [-mp] <cmd> [args...]") +func (p Pipe) Execute(args []string) error { + if p.Full && p.Part { + return errors.New("-m and -p are mutually exclusive") } provider := app.SelectedTabContent().(app.ProvidesMessage) - if !pipeFull && !pipePart { + if !p.Full && !p.Part { if _, ok := provider.(*app.MessageViewer); ok { - pipePart = true + p.Part = true } else if _, ok := provider.(*app.AccountView); ok { - pipeFull = true + p.Full = true } else { return errors.New( "Neither -m nor -p specified and cannot infer default") @@ -77,7 +53,7 @@ func (Pipe) Execute(args []string) error { } doTerm := func(reader io.Reader, name string) { - term, err := commands.QuickTerm(cmd, reader) + term, err := commands.QuickTerm(p.Command, reader) if err != nil { app.PushError(err.Error()) return @@ -86,7 +62,7 @@ func (Pipe) Execute(args []string) error { } doExec := func(reader io.Reader) { - ecmd := exec.Command(cmd[0], cmd[1:]...) + ecmd := exec.Command(p.Command[0], p.Command[1:]...) pipe, err := ecmd.StdinPipe() if err != nil { return @@ -106,17 +82,19 @@ func (Pipe) Execute(args []string) error { } else { if ecmd.ProcessState.ExitCode() != 0 { app.PushError(fmt.Sprintf( - "%s: completed with status %d", cmd[0], + "%s: completed with status %d", p.Command[0], ecmd.ProcessState.ExitCode())) } else { app.PushStatus(fmt.Sprintf( - "%s: completed with status %d", cmd[0], + "%s: completed with status %d", p.Command[0], ecmd.ProcessState.ExitCode()), 10*time.Second) } } } - if pipeFull { + app.PushStatus("Fetching messages ...", 10*time.Second) + + if p.Full { var uids []uint32 var title string @@ -125,12 +103,12 @@ func (Pipe) Execute(args []string) error { if err != nil { if mv, ok := provider.(*app.MessageViewer); ok { mv.MessageView().FetchFull(func(reader io.Reader) { - if background { + if p.Background { doExec(reader) } else { doTerm(reader, fmt.Sprintf("%s <%s", - cmd[0], title)) + p.Command[0], title)) } }) return nil @@ -202,27 +180,27 @@ func (Pipe) Execute(args []string) error { } reader := newMessagesReader(messages, len(messages) > 1) - if background { + if p.Background { doExec(reader) } else { - doTerm(reader, fmt.Sprintf("%s <%s", cmd[0], title)) + doTerm(reader, fmt.Sprintf("%s <%s", p.Command[0], title)) } }() - } else if pipePart { + } else if p.Part { mv, ok := provider.(*app.MessageViewer) if !ok { return fmt.Errorf("can only pipe message part from a message view") } - p := provider.SelectedMessagePart() - if p == nil { + part := provider.SelectedMessagePart() + if part == nil { return fmt.Errorf("could not fetch message part") } - mv.MessageView().FetchBodyPart(p.Index, func(reader io.Reader) { - if background { + mv.MessageView().FetchBodyPart(part.Index, func(reader io.Reader) { + if p.Background { doExec(reader) } else { name := fmt.Sprintf("%s <%s/[%d]", - cmd[0], p.Msg.Envelope.Subject, p.Index) + p.Command[0], part.Msg.Envelope.Subject, part.Index) doTerm(reader, name) } }) diff --git a/commands/msg/read.go b/commands/msg/read.go index bac2ceb3..e55ed00e 100644 --- a/commands/msg/read.go +++ b/commands/msg/read.go @@ -2,16 +2,20 @@ package msg import ( "fmt" + "strings" "time" - "git.sr.ht/~sircmpwn/getopt" - "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/models" "git.sr.ht/~rjarry/aerc/worker/types" ) -type FlagMsg struct{} +type FlagMsg struct { + Toggle bool `opt:"-t"` + Answered bool `opt:"-a" aliases:"flag,unflag"` + Flag models.Flags `opt:"-x" aliases:"flag,unflag" action:"ParseFlag"` + FlagName string +} func init() { register(FlagMsg{}) @@ -25,6 +29,23 @@ func (FlagMsg) Complete(args []string) []string { return nil } +func (f *FlagMsg) ParseFlag(arg string) error { + switch strings.ToLower(arg) { + case "seen": + f.Flag = models.SeenFlag + f.FlagName = "seen" + case "answered": + f.Flag = models.AnsweredFlag + f.FlagName = "answered" + case "flagged": + f.Flag = models.FlaggedFlag + f.FlagName = "flagged" + default: + return fmt.Errorf("Unknown flag %q", arg) + } + return nil +} + // 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 @@ -32,85 +53,20 @@ func (FlagMsg) Complete(args []string) []string { // // If this was called as 'read' or 'unread', it has the same effect as // 'flag' or 'unflag', respectively, but the 'Seen' flag is affected. -func (FlagMsg) Execute(args []string) error { - // The flag to change - var flag models.Flags - // User-readable name of the flag to change - var flagName string - // Whether to toggle the flag (true) or to enable/disable it (false) - var toggle bool - // Whether to enable (true) or disable (false) the flag - enable := (args[0] == "read" || args[0] == "flag") +func (f FlagMsg) Execute(args []string) error { // User-readable name for the action being performed var actionName string - // Getopt option string, varies by command name - var getoptString string - // Help message to provide on parsing failure - var helpMessage string - // Used during parsing to prevent choosing a flag muliple times - // A default flag will be used if this is false - flagChosen := false - - if args[0] == "read" || args[0] == "unread" { - flag = models.SeenFlag - flagName = "read" - getoptString = "t" - helpMessage = "Usage: " + args[0] + " [-t]" - } else { // 'flag' / 'unflag' - flag = models.FlaggedFlag - flagName = "flagged" - getoptString = "tax:" - helpMessage = "Usage: " + args[0] + " [-t] [-a | -x <flag>]" - } - opts, optind, err := getopt.Getopts(args, getoptString) - if err != nil { - return err - } - for _, opt := range opts { - switch opt.Option { - case 't': - toggle = true - case 'a': - if flagChosen { - return fmt.Errorf("Cannot choose a flag multiple times! " + helpMessage) - } - flag = models.AnsweredFlag - flagName = "answered" - flagChosen = true - case 'x': - if flagChosen { - return fmt.Errorf("Cannot choose a flag multiple times! " + helpMessage) - } - // TODO: Support all flags? - switch opt.Value { - case "Seen": - flag = models.SeenFlag - flagName = "seen" - case "Answered": - flag = models.AnsweredFlag - flagName = "answered" - case "Flagged": - flag = models.FlaggedFlag - flagName = "flagged" - default: - return fmt.Errorf("Unknown / Prohibited flag \"%v\"", opt.Value) - } - flagChosen = true + switch args[0] { + case "read", "unread": + f.Flag = models.SeenFlag + f.FlagName = "seen" + case "flag", "unflag": + if f.Flag == 0 { + f.Flag = models.FlaggedFlag + f.FlagName = "flagged" } } - switch { - case toggle: - actionName = "Toggling" - case enable: - actionName = "Setting" - default: - actionName = "Unsetting" - } - if optind != len(args) { - // Any non-option arguments: Error - return fmt.Errorf(helpMessage) - } h := newHelper() store, err := h.store() @@ -122,7 +78,7 @@ func (FlagMsg) Execute(args []string) error { var toEnable []uint32 var toDisable []uint32 - if toggle { + if f.Toggle { // If toggling, split messages into those that need to // be enabled / disabled. msgs, err := h.messages() @@ -130,29 +86,35 @@ func (FlagMsg) Execute(args []string) error { return err } for _, m := range msgs { - if m.Flags.Has(flag) { + if m.Flags.Has(f.Flag) { toDisable = append(toDisable, m.Uid) } else { toEnable = append(toEnable, m.Uid) } } + actionName = "Toggling" } else { msgUids, err := h.markedOrSelectedUids() if err != nil { return err } - if enable { + switch args[0] { + case "read", "flag": toEnable = msgUids - } else { + actionName = "Setting" + default: toDisable = msgUids + actionName = "Unsetting" } } + status := fmt.Sprintf("%s flag %q successful", actionName, f.FlagName) + if len(toEnable) != 0 { - store.Flag(toEnable, flag, true, func(msg types.WorkerMessage) { + store.Flag(toEnable, f.Flag, true, func(msg types.WorkerMessage) { switch msg := msg.(type) { case *types.Done: - app.PushStatus(actionName+" flag '"+flagName+"' successful", 10*time.Second) + app.PushStatus(status, 10*time.Second) store.Marker().ClearVisualMark() case *types.Error: app.PushError(msg.Error.Error()) @@ -160,10 +122,10 @@ func (FlagMsg) Execute(args []string) error { }) } if len(toDisable) != 0 { - store.Flag(toDisable, flag, false, func(msg types.WorkerMessage) { + store.Flag(toDisable, f.Flag, false, func(msg types.WorkerMessage) { switch msg := msg.(type) { case *types.Done: - app.PushStatus(actionName+" flag '"+flagName+"' successful", 10*time.Second) + app.PushStatus(status, 10*time.Second) store.Marker().ClearVisualMark() case *types.Error: app.PushError(msg.Error.Error()) diff --git a/commands/msg/recall.go b/commands/msg/recall.go index 2d999b1d..3b78a763 100644 --- a/commands/msg/recall.go +++ b/commands/msg/recall.go @@ -15,10 +15,13 @@ import ( "git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/worker/types" - "git.sr.ht/~sircmpwn/getopt" ) -type Recall struct{} +type Recall struct { + Force bool `opt:"-f"` + Edit bool `opt:"-e"` + NoEdit bool `opt:"-E"` +} func init() { register(Recall{}) @@ -32,34 +35,15 @@ func (Recall) Complete(args []string) []string { return nil } -func (Recall) Execute(args []string) error { - force := false - editHeaders := config.Compose.EditHeaders - - opts, optind, err := getopt.Getopts(args, "feE") - if err != nil { - return err - } - for _, opt := range opts { - switch opt.Option { - case 'f': - force = true - case 'e': - editHeaders = true - case 'E': - editHeaders = false - } - } - if len(args) != optind { - return errors.New("Usage: recall [-f] [-e|-E]") - } +func (r Recall) Execute(args []string) error { + editHeaders := (config.Compose.EditHeaders || r.Edit) && !r.NoEdit widget := app.SelectedTabContent().(app.ProvidesMessage) acct := widget.SelectedAccount() if acct == nil { return errors.New("No account selected") } - if acct.SelectedDirectory() != acct.AccountConfig().Postpone && !force { + if acct.SelectedDirectory() != acct.AccountConfig().Postpone && !r.Force { return errors.New("Use -f to recall from outside the " + acct.AccountConfig().Postpone + " directory.") } @@ -164,7 +148,7 @@ func (Recall) Execute(args []string) error { }) } - if force { + if r.Force { composer.SetRecalledFrom(acct.SelectedDirectory()) } diff --git a/commands/msg/reply.go b/commands/msg/reply.go index fff90fff..2ab9e9f8 100644 --- a/commands/msg/reply.go +++ b/commands/msg/reply.go @@ -9,8 +9,6 @@ import ( "strings" "time" - "git.sr.ht/~sircmpwn/getopt" - "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/commands/account" "git.sr.ht/~rjarry/aerc/config" @@ -24,7 +22,14 @@ import ( "github.com/emersion/go-message/mail" ) -type reply struct{} +type reply struct { + All bool `opt:"-a"` + Close bool `opt:"-c"` + Quote bool `opt:"-q"` + Template string `opt:"-T"` + Edit bool `opt:"-e"` + NoEdit bool `opt:"-E"` +} func init() { register(reply{}) @@ -38,37 +43,8 @@ func (reply) Complete(args []string) []string { return nil } -func (reply) Execute(args []string) error { - opts, optind, err := getopt.Getopts(args, "acqT:eE") - if err != nil { - return err - } - if optind != len(args) { - return errors.New("Usage: reply [-acq -T <template>] [-e|-E]") - } - var ( - quote bool - replyAll bool - closeOnReply bool - template string - ) - editHeaders := config.Compose.EditHeaders - for _, opt := range opts { - switch opt.Option { - case 'a': - replyAll = true - case 'c': - closeOnReply = true - case 'q': - quote = true - case 'T': - template = opt.Value - case 'e': - editHeaders = true - case 'E': - editHeaders = false - } - } +func (r reply) Execute(args []string) error { + editHeaders := (config.Compose.EditHeaders || r.Edit) && !r.NoEdit widget := app.SelectedTabContent().(app.ProvidesMessage) acct := widget.SelectedAccount() @@ -116,7 +92,7 @@ func (reply) Execute(args []string) error { recSet.AddList(to) - if replyAll { + if r.All { // order matters, due to the deduping // in order of importance, first parse the To, then the Cc header @@ -165,12 +141,12 @@ func (reply) Execute(args []string) error { addTab := func() error { composer, err := app.NewComposer(acct, acct.AccountConfig(), acct.Worker(), editHeaders, - template, h, &original, nil) + r.Template, h, &original, nil) if err != nil { app.PushError("Error: " + err.Error()) return err } - if mv != nil && closeOnReply { + if mv != nil && r.Close { app.RemoveTab(mv, true) } @@ -190,18 +166,19 @@ func (reply) Execute(args []string) error { } case c.Sent(): store.Answered([]uint32{msg.Uid}, true, nil) - case mv != nil && closeOnReply: + case mv != nil && r.Close: + view := account.ViewMessage{Peek: true} //nolint:errcheck // who cares? - account.ViewMessage{}.Execute([]string{"-p"}) + view.Execute([]string{"view", "-p"}) } }) return nil } - if quote { - if template == "" { - template = config.Templates.QuotedReply + if r.Quote { + if r.Template == "" { + r.Template = config.Templates.QuotedReply } if crypto.IsEncrypted(msg.BodyStructure) { @@ -256,8 +233,8 @@ func (reply) Execute(args []string) error { }) return nil } else { - if template == "" { - template = config.Templates.NewMessage + if r.Template == "" { + r.Template = config.Templates.NewMessage } return addTab() } diff --git a/commands/msg/toggle-thread-context.go b/commands/msg/toggle-thread-context.go index 7530bc9e..0ef6778f 100644 --- a/commands/msg/toggle-thread-context.go +++ b/commands/msg/toggle-thread-context.go @@ -1,8 +1,6 @@ package msg import ( - "errors" - "git.sr.ht/~rjarry/aerc/lib/ui" ) @@ -21,9 +19,6 @@ func (ToggleThreadContext) Complete(args []string) []string { } func (ToggleThreadContext) Execute(args []string) error { - if len(args) != 1 { - return errors.New("Usage: toggle-entire-thread") - } h := newHelper() store, err := h.store() if err != nil { diff --git a/commands/msg/toggle-threads.go b/commands/msg/toggle-threads.go index 1d38685a..88cc763f 100644 --- a/commands/msg/toggle-threads.go +++ b/commands/msg/toggle-threads.go @@ -1,8 +1,6 @@ package msg import ( - "errors" - "git.sr.ht/~rjarry/aerc/lib/state" "git.sr.ht/~rjarry/aerc/lib/ui" ) @@ -22,9 +20,6 @@ func (ToggleThreads) Complete(args []string) []string { } func (ToggleThreads) Execute(args []string) error { - if len(args) != 1 { - return errors.New("Usage: toggle-threads") - } h := newHelper() acct, err := h.account() if err != nil { diff --git a/commands/msg/unsubscribe.go b/commands/msg/unsubscribe.go index 069aedab..a489b3b9 100644 --- a/commands/msg/unsubscribe.go +++ b/commands/msg/unsubscribe.go @@ -12,13 +12,15 @@ import ( "git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/log" - "git.sr.ht/~sircmpwn/getopt" "github.com/emersion/go-message/mail" ) // Unsubscribe helps people unsubscribe from mailing lists by way of the // List-Unsubscribe header. -type Unsubscribe struct{} +type Unsubscribe struct { + Edit bool `opt:"-e"` + NoEdit bool `opt:"-E"` +} func init() { register(Unsubscribe{}) @@ -35,23 +37,9 @@ func (Unsubscribe) Complete(args []string) []string { } // Execute runs the Unsubscribe command -func (Unsubscribe) Execute(args []string) error { - editHeaders := config.Compose.EditHeaders - opts, optind, err := getopt.Getopts(args, "eE") - if err != nil { - return err - } - if len(args) != optind { - return errors.New("Usage: unsubscribe [-e|-E]") - } - for _, opt := range opts { - switch opt.Option { - case 'e': - editHeaders = true - case 'E': - editHeaders = false - } - } +func (u Unsubscribe) Execute(args []string) error { + editHeaders := (config.Compose.EditHeaders || u.Edit) && !u.NoEdit + widget := app.SelectedTabContent().(app.ProvidesMessage) msg, err := widget.SelectedMessage() if err != nil { diff --git a/commands/msgview/close.go b/commands/msgview/close.go index e0ad6040..32702da9 100644 --- a/commands/msgview/close.go +++ b/commands/msgview/close.go @@ -1,8 +1,6 @@ package msgview import ( - "errors" - "git.sr.ht/~rjarry/aerc/app" ) @@ -21,9 +19,6 @@ func (Close) Complete(args []string) []string { } func (Close) Execute(args []string) error { - if len(args) != 1 { - return errors.New("Usage: close") - } mv, _ := app.SelectedTabContent().(*app.MessageViewer) app.RemoveTab(mv, true) return nil diff --git a/commands/msgview/next-part.go b/commands/msgview/next-part.go index 951842d7..9b1b1fcc 100644 --- a/commands/msgview/next-part.go +++ b/commands/msgview/next-part.go @@ -1,13 +1,12 @@ package msgview import ( - "fmt" - "strconv" - "git.sr.ht/~rjarry/aerc/app" ) -type NextPrevPart struct{} +type NextPrevPart struct { + Offset int `opt:"n" default:"1"` +} func init() { register(NextPrevPart{}) @@ -21,22 +20,9 @@ func (NextPrevPart) Complete(args []string) []string { return nil } -func (NextPrevPart) Execute(args []string) error { - if len(args) > 2 { - return nextPrevPartUsage(args[0]) - } - var ( - n int = 1 - err error - ) - if len(args) > 1 { - n, err = strconv.Atoi(args[1]) - if err != nil { - return nextPrevPartUsage(args[0]) - } - } +func (np NextPrevPart) Execute(args []string) error { mv, _ := app.SelectedTabContent().(*app.MessageViewer) - for ; n > 0; n-- { + for n := 0; n < np.Offset; n++ { if args[0] == "prev-part" { mv.PreviousPart() } else { @@ -45,7 +31,3 @@ func (NextPrevPart) Execute(args []string) error { } return nil } - -func nextPrevPartUsage(cmd string) error { - return fmt.Errorf("Usage: %s [n]", cmd) -} diff --git a/commands/msgview/next.go b/commands/msgview/next.go index a69cb8ee..d8f046f8 100644 --- a/commands/msgview/next.go +++ b/commands/msgview/next.go @@ -3,6 +3,8 @@ package msgview import ( "errors" "fmt" + "strconv" + "strings" "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/commands/account" @@ -11,12 +13,28 @@ import ( "git.sr.ht/~rjarry/aerc/worker/types" ) -type NextPrevMsg struct{} +type NextPrevMsg struct { + Amount int `opt:"n" default:"1" metavar:"N[%]" action:"ParseAmount"` + Percent bool +} func init() { register(NextPrevMsg{}) } +func (np *NextPrevMsg) ParseAmount(arg string) error { + if strings.HasSuffix(arg, "%") { + np.Percent = true + arg = strings.TrimSuffix(arg, "%") + } + i, err := strconv.ParseInt(arg, 10, 64) + if err != nil { + return err + } + np.Amount = int(i) + return nil +} + func (NextPrevMsg) Aliases() []string { return []string{"next", "next-message", "prev", "prev-message"} } @@ -25,11 +43,13 @@ func (NextPrevMsg) Complete(args []string) []string { return nil } -func (NextPrevMsg) Execute(args []string) error { - n, pct, err := account.ParseNextPrevMessage(args) +func (np NextPrevMsg) Execute(args []string) error { + cmd := account.NextPrevMsg{Amount: np.Amount, Percent: np.Percent} + err := cmd.Execute(args) if err != nil { return err } + mv, _ := app.SelectedTabContent().(*app.MessageViewer) acct := mv.SelectedAccount() if acct == nil { @@ -39,10 +59,6 @@ func (NextPrevMsg) Execute(args []string) error { if store == nil { return fmt.Errorf("Cannot perform action. No message store set.") } - err = account.ExecuteNextPrevMessage(args, acct, pct, n) - if err != nil { - return err - } executeNextPrev := func(nextMsg *models.MessageInfo) { lib.NewMessageStoreView(nextMsg, mv.MessageView().SeenFlagSet(), store, app.CryptoProvider(), app.DecryptKeys, diff --git a/commands/msgview/open-link.go b/commands/msgview/open-link.go index 7241fb40..ad2a7cc2 100644 --- a/commands/msgview/open-link.go +++ b/commands/msgview/open-link.go @@ -1,7 +1,6 @@ package msgview import ( - "errors" "fmt" "net/url" @@ -11,7 +10,10 @@ import ( "git.sr.ht/~rjarry/aerc/log" ) -type OpenLink struct{} +type OpenLink struct { + Url *url.URL `opt:"url" action:"ParseUrl"` + Cmd []string `opt:"..." required:"false"` +} func init() { register(OpenLink{}) @@ -31,18 +33,20 @@ func (OpenLink) Complete(args []string) []string { return nil } -func (OpenLink) Execute(args []string) error { - if len(args) < 2 { - return errors.New("Usage: open-link <url> [program [args...]]") - } - u, err := url.Parse(args[1]) +func (o *OpenLink) ParseUrl(arg string) error { + u, err := url.Parse(arg) if err != nil { return err } - mime := fmt.Sprintf("x-scheme-handler/%s", u.Scheme) + o.Url = u + return nil +} + +func (o OpenLink) Execute(args []string) error { + mime := fmt.Sprintf("x-scheme-handler/%s", o.Url.Scheme) go func() { defer log.PanicHandler() - if err := lib.XDGOpenMime(args[1], mime, args[2:]); err != nil { + if err := lib.XDGOpenMime(o.Url.String(), mime, o.Cmd); err != nil { app.PushError("open-link: " + err.Error()) } }() diff --git a/commands/msgview/open.go b/commands/msgview/open.go index 6c806c7c..bab46bd7 100644 --- a/commands/msgview/open.go +++ b/commands/msgview/open.go @@ -7,14 +7,15 @@ import ( "os" "path/filepath" - "git.sr.ht/~sircmpwn/getopt" - "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/log" ) -type Open struct{} +type Open struct { + Delete bool `opt:"-d"` + Cmd []string `opt:"..." required:"false"` +} func init() { register(Open{}) @@ -33,19 +34,6 @@ func (Open) Complete(args []string) []string { } func (o Open) Execute(args []string) error { - opts, optind, err := getopt.Getopts(args, o.Options()) - if err != nil { - return err - } - - del := false - - for _, opt := range opts { - if opt.Option == 'd' { - del = true - } - } - mv := app.SelectedTabContent().(*app.MessageViewer) if mv == nil { return errors.New("open only supported selected message parts") @@ -84,10 +72,10 @@ func (o Open) Execute(args []string) error { go func() { defer log.PanicHandler() - if del { + if o.Delete { defer os.Remove(tmpFile.Name()) } - err = lib.XDGOpenMime(tmpFile.Name(), mimeType, args[optind:]) + err = lib.XDGOpenMime(tmpFile.Name(), mimeType, o.Cmd) if err != nil { app.PushError("open: " + err.Error()) } diff --git a/commands/msgview/save.go b/commands/msgview/save.go index c8e00e4f..ea7599d3 100644 --- a/commands/msgview/save.go +++ b/commands/msgview/save.go @@ -9,8 +9,6 @@ import ( "strings" "time" - "git.sr.ht/~sircmpwn/getopt" - "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/commands" "git.sr.ht/~rjarry/aerc/config" @@ -19,7 +17,13 @@ import ( "git.sr.ht/~rjarry/aerc/models" ) -type Save struct{} +type Save struct { + Force bool `opt:"-f"` + CreateDirs bool `opt:"-p"` + Attachments bool `opt:"-a"` + AllAttachments bool `opt:"-A"` + Path string `opt:"..." required:"false" metavar:"<path>"` +} func init() { register(Save{}) @@ -43,77 +47,33 @@ func (s Save) Complete(args []string) []string { return commands.CompletePath(xdg.ExpandHome(path)) } -type saveParams struct { - force bool - createDirs bool - trailingSlash bool - attachments bool - allAttachments bool -} - func (s Save) Execute(args []string) error { - opts, optind, err := getopt.Getopts(args, s.Options()) - if err != nil { - return err - } - - var params saveParams - - for _, opt := range opts { - switch opt.Option { - case 'f': - params.force = true - case 'p': - params.createDirs = true - case 'a': - params.attachments = true - case 'A': - params.allAttachments = true - } - } - - defaultPath := config.General.DefaultSavePath // we either need a path or a defaultPath - if defaultPath == "" && len(args) == optind { - return errors.New("Usage: :save [-fpa] <path>") - } - - // as a convenience we join with spaces, so that the user doesn't need to - // quote filenames containing spaces - path := strings.Join(args[optind:], " ") - - // needs to be determined prior to calling filepath.Clean / filepath.Join - // it gets stripped by Clean. - // we auto generate a name if a directory was given - if len(path) > 0 { - params.trailingSlash = path[len(path)-1] == '/' - } else if len(defaultPath) > 0 && len(path) == 0 { - // empty path, so we might have a default that ends in a trailingSlash - params.trailingSlash = defaultPath[len(defaultPath)-1] == '/' + if s.Path == "" && config.General.DefaultSavePath == "" { + return errors.New("No default save path in config") } // Absolute paths are taken as is so that the user can override the default // if they want to - if !isAbsPath(path) { - path = filepath.Join(defaultPath, path) + if !isAbsPath(s.Path) { + s.Path = filepath.Join(config.General.DefaultSavePath, s.Path) } - path = xdg.ExpandHome(path) + s.Path = xdg.ExpandHome(s.Path) mv, ok := app.SelectedTabContent().(*app.MessageViewer) if !ok { return fmt.Errorf("SelectedTabContent is not a MessageViewer") } - if params.attachments || params.allAttachments { - parts := mv.AttachmentParts(params.allAttachments) + if s.Attachments || s.AllAttachments { + parts := mv.AttachmentParts(s.AllAttachments) if len(parts) == 0 { return fmt.Errorf("This message has no attachments") } - params.trailingSlash = true names := make(map[string]struct{}) for _, pi := range parts { - if err := savePart(pi, path, mv, ¶ms, names); err != nil { + if err := s.savePart(pi, mv, names); err != nil { return err } } @@ -121,23 +81,22 @@ func (s Save) Execute(args []string) error { } pi := mv.SelectedMessagePart() - return savePart(pi, path, mv, ¶ms, make(map[string]struct{})) + return s.savePart(pi, mv, make(map[string]struct{})) } -func savePart( +func (s *Save) savePart( pi *app.PartInfo, - path string, mv *app.MessageViewer, - params *saveParams, names map[string]struct{}, ) error { - if params.trailingSlash || isDirExists(path) { + path := s.Path + if s.Attachments || s.AllAttachments || isDirExists(path) { filename := generateFilename(pi.Part) path = filepath.Join(path, filename) } dir := filepath.Dir(path) - if params.createDirs && dir != "" { + if s.CreateDirs && dir != "" { err := os.MkdirAll(dir, 0o755) if err != nil { return err @@ -147,7 +106,7 @@ func savePart( path = getCollisionlessFilename(path, names) names[path] = struct{}{} - if pathExists(path) && !params.force { + if pathExists(path) && !s.Force { return fmt.Errorf("%q already exists and -f not given", path) } diff --git a/commands/msgview/toggle-headers.go b/commands/msgview/toggle-headers.go index 746692fd..c27307d3 100644 --- a/commands/msgview/toggle-headers.go +++ b/commands/msgview/toggle-headers.go @@ -1,8 +1,6 @@ package msgview import ( - "fmt" - "git.sr.ht/~rjarry/aerc/app" ) @@ -21,14 +19,7 @@ func (ToggleHeaders) Complete(args []string) []string { } func (ToggleHeaders) Execute(args []string) error { - if len(args) > 1 { - return toggleHeadersUsage(args[0]) - } mv, _ := app.SelectedTabContent().(*app.MessageViewer) mv.ToggleHeaders() return nil } - -func toggleHeadersUsage(cmd string) error { - return fmt.Errorf("Usage: %s", cmd) -} diff --git a/commands/msgview/toggle-key-passthrough.go b/commands/msgview/toggle-key-passthrough.go index 1524b420..32735870 100644 --- a/commands/msgview/toggle-key-passthrough.go +++ b/commands/msgview/toggle-key-passthrough.go @@ -1,8 +1,6 @@ package msgview import ( - "errors" - "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/lib/state" ) @@ -22,9 +20,6 @@ func (ToggleKeyPassthrough) Complete(args []string) []string { } func (ToggleKeyPassthrough) Execute(args []string) error { - if len(args) != 1 { - return errors.New("Usage: toggle-key-passthrough") - } mv, _ := app.SelectedTabContent().(*app.MessageViewer) keyPassthroughEnabled := mv.ToggleKeyPassthrough() if acct := mv.SelectedAccount(); acct != nil { diff --git a/commands/new-account.go b/commands/new-account.go index 0f84141f..b30e0e34 100644 --- a/commands/new-account.go +++ b/commands/new-account.go @@ -1,13 +1,12 @@ package commands import ( - "errors" - "git.sr.ht/~rjarry/aerc/app" - "git.sr.ht/~sircmpwn/getopt" ) -type NewAccount struct{} +type NewAccount struct { + Temp bool `opt:"-t"` +} func init() { register(NewAccount{}) @@ -21,17 +20,9 @@ func (NewAccount) Complete(args []string) []string { return nil } -func (NewAccount) Execute(args []string) error { - opts, _, err := getopt.Getopts(args, "t") - if err != nil { - return errors.New("Usage: new-account [-t]") - } +func (n NewAccount) Execute(args []string) error { wizard := app.NewAccountWizard() - for _, opt := range opts { - if opt.Option == 't' { - wizard.ConfigureTemporaryAccount(true) - } - } + wizard.ConfigureTemporaryAccount(n.Temp) wizard.Focus(true) app.NewTab(wizard, "New account") return nil diff --git a/commands/next-tab.go b/commands/next-tab.go index eb7b1ed1..d8374191 100644 --- a/commands/next-tab.go +++ b/commands/next-tab.go @@ -1,13 +1,12 @@ package commands import ( - "fmt" - "strconv" - "git.sr.ht/~rjarry/aerc/app" ) -type NextPrevTab struct{} +type NextPrevTab struct { + Offset int `opt:"n" default:"1"` +} func init() { register(NextPrevTab{}) @@ -21,21 +20,8 @@ func (NextPrevTab) Complete(args []string) []string { return nil } -func (NextPrevTab) Execute(args []string) error { - if len(args) > 2 { - return nextPrevTabUsage(args[0]) - } - var ( - n int = 1 - err error - ) - if len(args) > 1 { - n, err = strconv.Atoi(args[1]) - if err != nil { - return nextPrevTabUsage(args[0]) - } - } - for ; n > 0; n-- { +func (np NextPrevTab) Execute(args []string) error { + for n := 0; n < np.Offset; n++ { if args[0] == "prev-tab" { app.PrevTab() } else { @@ -45,7 +31,3 @@ func (NextPrevTab) Execute(args []string) error { app.UpdateStatus() return nil } - -func nextPrevTabUsage(cmd string) error { - return fmt.Errorf("Usage: %s [n]", cmd) -} diff --git a/commands/pin-tab.go b/commands/pin-tab.go index 7a3258d4..276442ce 100644 --- a/commands/pin-tab.go +++ b/commands/pin-tab.go @@ -1,8 +1,6 @@ package commands import ( - "fmt" - "git.sr.ht/~rjarry/aerc/app" ) @@ -21,10 +19,6 @@ func (PinTab) Complete(args []string) []string { } func (PinTab) Execute(args []string) error { - if len(args) != 1 { - return fmt.Errorf("Usage: %s", args[0]) - } - switch args[0] { case "pin-tab": app.PinTab() diff --git a/commands/prompt.go b/commands/prompt.go index 0d10ffa0..d42a6597 100644 --- a/commands/prompt.go +++ b/commands/prompt.go @@ -1,7 +1,6 @@ package commands import ( - "fmt" "strings" "git.sr.ht/~rjarry/go-opt" @@ -9,7 +8,10 @@ import ( "git.sr.ht/~rjarry/aerc/app" ) -type Prompt struct{} +type Prompt struct { + Text string `opt:"text"` + Cmd []string `opt:"..."` +} func init() { register(Prompt{}) @@ -71,13 +73,8 @@ func (Prompt) Complete(args []string) []string { return rs } -func (Prompt) Execute(args []string) error { - if len(args) < 3 { - return fmt.Errorf("Usage: %s <prompt> <cmd>", args[0]) - } - - prompt := args[1] - cmd := opt.QuoteArgs(args[2:]...) - app.RegisterPrompt(prompt, cmd.String()) +func (p Prompt) Execute(args []string) error { + cmd := opt.QuoteArgs(p.Cmd...) + app.RegisterPrompt(p.Text, cmd.String()) return nil } diff --git a/commands/pwd.go b/commands/pwd.go index 9b562313..426be78b 100644 --- a/commands/pwd.go +++ b/commands/pwd.go @@ -1,7 +1,6 @@ package commands import ( - "errors" "os" "time" @@ -23,9 +22,6 @@ func (PrintWorkDir) Complete(args []string) []string { } func (PrintWorkDir) Execute(args []string) error { - if len(args) != 1 { - return errors.New("Usage: pwd") - } pwd, err := os.Getwd() if err != nil { return err diff --git a/commands/quit.go b/commands/quit.go index 42e8fc95..3e0f2968 100644 --- a/commands/quit.go +++ b/commands/quit.go @@ -1,14 +1,14 @@ package commands import ( - "errors" "fmt" "git.sr.ht/~rjarry/aerc/commands/mode" - "git.sr.ht/~sircmpwn/getopt" ) -type Quit struct{} +type Quit struct { + Force bool `opt:"-f"` +} func init() { register(Quit{}) @@ -28,21 +28,8 @@ func (err ErrorExit) Error() string { return "exit" } -func (Quit) Execute(args []string) error { - force := false - opts, optind, err := getopt.Getopts(args, "f") - if err != nil { - return err - } - for _, opt := range opts { - if opt.Option == 'f' { - force = true - } - } - if len(args) != optind { - return errors.New("Usage: quit [-f]") - } - if force || mode.QuitAllowed() { +func (q Quit) Execute(args []string) error { + if q.Force || mode.QuitAllowed() { return ErrorExit(1) } return fmt.Errorf("A task is not done yet. Use -f to force an exit.") diff --git a/commands/send-keys.go b/commands/send-keys.go index f9cae392..9541b88d 100644 --- a/commands/send-keys.go +++ b/commands/send-keys.go @@ -1,15 +1,15 @@ package commands import ( - "strings" - "git.sr.ht/~rjarry/aerc/app" "git.sr.ht/~rjarry/aerc/config" "github.com/gdamore/tcell/v2" "github.com/pkg/errors" ) -type SendKeys struct{} +type SendKeys struct { + Keys string `opt:"..."` +} func init() { register(SendKeys{}) @@ -23,7 +23,7 @@ func (SendKeys) Complete(args []string) []string { return nil } -func (SendKeys) Execute(args []string) error { +func (s SendKeys) Execute(args []string) error { tab, ok := app.SelectedTabContent().(app.HasTerminal) if !ok { return errors.New("There is no terminal here") @@ -34,10 +34,9 @@ func (SendKeys) Execute(args []string) error { return errors.New("The terminal is not active") } - text2send := strings.Join(args[1:], "") - keys2send, err := config.ParseKeyStrokes(text2send) + keys2send, err := config.ParseKeyStrokes(s.Keys) if err != nil { - return errors.Wrapf(err, "Unable to parse keystroke: '%s'", text2send) + return errors.Wrapf(err, "Unable to parse keystroke: %q", s.Keys) } for _, key := range keys2send { diff --git a/commands/term.go b/commands/term.go index 26e277db..225fee57 100644 --- a/commands/term.go +++ b/commands/term.go @@ -9,7 +9,9 @@ import ( "git.sr.ht/~rjarry/aerc/lib/ui" ) -type Term struct{} +type Term struct { + Cmd []string `opt:"..." required:"false"` +} func init() { register(Term{}) @@ -23,23 +25,22 @@ func (Term) Complete(args []string) []string { return nil } -// The help command is an alias for `term man` thus Term requires a simple func -func TermCore(args []string) error { - if len(args) == 1 { +func (t Term) Execute(args []string) error { + if len(t.Cmd) == 0 { shell, err := loginshell.Shell() if err != nil { return err } - args = append(args, shell) + t.Cmd = []string{shell} } - term, err := app.NewTerminal(exec.Command(args[1], args[2:]...)) + term, err := app.NewTerminal(exec.Command(t.Cmd[0], t.Cmd[1:]...)) if err != nil { return err } - tab := app.NewTab(term, args[1]) + tab := app.NewTab(term, t.Cmd[0]) term.OnTitle = func(title string) { if title == "" { - title = args[1] + title = t.Cmd[0] } if tab.Name != title { tab.Name = title @@ -54,7 +55,3 @@ func TermCore(args []string) error { } return nil } - -func (Term) Execute(args []string) error { - return TermCore(args) -} diff --git a/commands/terminal/close.go b/commands/terminal/close.go index 5d52bbc5..913a3387 100644 --- a/commands/terminal/close.go +++ b/commands/terminal/close.go @@ -1,8 +1,6 @@ package terminal import ( - "errors" - "git.sr.ht/~rjarry/aerc/app" ) @@ -21,9 +19,6 @@ func (Close) Complete(args []string) []string { } func (Close) Execute(args []string) error { - if len(args) != 1 { - return errors.New("Usage: close") - } term, _ := app.SelectedTabContent().(*app.Terminal) term.Close() return nil diff --git a/commands/z.go b/commands/z.go index 966e2ce9..5aee2a2c 100644 --- a/commands/z.go +++ b/commands/z.go @@ -7,7 +7,9 @@ import ( "strings" ) -type Zoxide struct{} +type Zoxide struct { + Target string `opt:"..." default:"~" metavar:"<folder> | <query>..."` +} func ZoxideAdd(arg string) error { zargs := []string{"add", arg} @@ -40,15 +42,9 @@ func (Zoxide) Complete(args []string) []string { // Execute calls zoxide add and query and delegates actually changing the // directory to ChangeDirectory -func (Zoxide) Execute(args []string) error { - if len(args) < 1 { - return errors.New("Usage: z [directory or zoxide query]") - } - target := strings.Join(args[1:], " ") - switch target { - case "": - return ChangeDirectory{}.Execute(args) - case "-": +func (z Zoxide) Execute(args []string) error { + switch z.Target { + case "-", "~": if previousDir != "" { err := ZoxideAdd(previousDir) if err != nil { @@ -57,7 +53,7 @@ func (Zoxide) Execute(args []string) error { } return ChangeDirectory{}.Execute(args) default: - _, err := os.Stat(target) + _, err := os.Stat(z.Target) if err != nil { // not a file, assume zoxide query res, err := ZoxideQuery(args) @@ -68,11 +64,12 @@ func (Zoxide) Execute(args []string) error { if err != nil { return err } - return ChangeDirectory{}.Execute([]string{"z", res}) + cd := ChangeDirectory{Target: res} + return cd.Execute([]string{"z", res}) } } else { - err := ZoxideAdd(target) + err := ZoxideAdd(z.Target) if err != nil { return err } |