Markus Unkel: 1 composer: add focus-body option 12 files changed, 90 insertions(+), 31 deletions(-)
Hi Markus, Markus Unkel, Nov 09, 2024 at 14:16:
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~rjarry/aerc-devel/patches/55891/mbox | git am -3Learn more about email & git
When the composer window opens, an user might want to start writing the email body before adding a subject and recipients. Setting the focus-body option to true achieves that by setting the focus to the editor. The compose options edit-headers and focus-body are mutually exclusive, because the focus-body option implies the focus to the editor and not the focus to the line in editor, where the email body starts, in case of having the edit-headers option set, where also the headers are directly editable in editor. Signed-off-by: Markus Unkel <markus@unkel.io> --- This adds the mutual exclusivity of both compose options edit-headers and focus-body, and the focus-body option to all compose related commands. app/aerc.go | 4 ++-- app/compose.go | 9 ++++++--- commands/account/compose.go | 18 ++++++++++++------ commands/account/recover.go | 3 ++- commands/msg/forward.go | 9 ++++++++- commands/msg/invite.go | 9 +++++++-- commands/msg/recall.go | 14 ++++++++++---- commands/msg/reply.go | 22 ++++++++++++++-------- commands/msg/unsubscribe.go | 15 +++++++++++---- config/aerc.conf | 6 ++++++ config/compose.go | 6 ++++++ doc/aerc-config.5.scd | 6 ++++++ 12 files changed, 90 insertions(+), 31 deletions(-) diff --git a/app/aerc.go b/app/aerc.go index 12bb6893..650252ee 100644 --- a/app/aerc.go +++ b/app/aerc.go @@ -799,8 +799,8 @@ func (aerc *Aerc) mailto(addr *url.URL) error { composer, err := NewComposer(acct, acct.AccountConfig(), acct.Worker(), - config.Compose.EditHeaders, template, h, nil, - strings.NewReader(body)) + config.Compose.EditHeaders, config.Compose.FocusBody, + template, h, nil, strings.NewReader(body)) if err != nil { return err } diff --git a/app/compose.go b/app/compose.go index 7a581505..a31a1061 100644 --- a/app/compose.go +++ b/app/compose.go @@ -59,6 +59,7 @@ type Composer struct { encrypt bool attachKey bool editHeaders bool + focusBody bool layout HeaderLayout focusable []ui.MouseableDrawableInteractive @@ -79,8 +80,9 @@ type Composer struct { func NewComposer( acct *AccountView, acctConfig *config.AccountConfig, - worker *types.Worker, editHeaders bool, template string, - h *mail.Header, orig *models.OriginalMail, body io.Reader, + worker *types.Worker, editHeaders bool, focusBody bool, + template string, h *mail.Header, orig *models.OriginalMail, + body io.Reader, ) (*Composer, error) { if h == nil { h = new(mail.Header) @@ -105,6 +107,7 @@ func NewComposer( completer: nil, editHeaders: editHeaders, + focusBody: focusBody, } data := state.NewDataSetter() @@ -1276,7 +1279,7 @@ func (c *Composer) showTerminal() error { c.focusable = append(c.focusable, c.editor) c.review = nil c.updateGrid() - if c.editHeaders { + if c.editHeaders || c.focusBody { c.focusTerminalPriv() } return nil diff --git a/commands/account/compose.go b/commands/account/compose.go index 5e5d3e0f..496feff3 100644 --- a/commands/account/compose.go +++ b/commands/account/compose.go @@ -16,11 +16,12 @@ import ( ) type Compose struct { - Headers string `opt:"-H" action:"ParseHeader" desc:"Add the specified header to the message."` - Template string `opt:"-T" complete:"CompleteTemplate" desc:"Template name."` - Edit bool `opt:"-e" desc:"Force [compose].edit-headers = true."` - NoEdit bool `opt:"-E" desc:"Force [compose].edit-headers = false."` - Body string `opt:"..." required:"false"` + Headers string `opt:"-H" action:"ParseHeader" desc:"Add the specified header to the message."` + Template string `opt:"-T" complete:"CompleteTemplate" desc:"Template name."` + Edit bool `opt:"-e" desc:"Force [compose].edit-headers = true."` + NoEdit bool `opt:"-E" desc:"Force [compose].edit-headers = false."` + FocusBody bool `opt:"-f" desc:"Force [compose].focus-body = true."` + Body string `opt:"..." required:"false"` } func init() { @@ -66,6 +67,11 @@ func (c Compose) Execute(args []string) error { c.Template = config.Templates.NewMessage } editHeaders := (config.Compose.EditHeaders || c.Edit) && !c.NoEdit + focusBody := config.Compose.FocusBody || c.FocusBody + + if editHeaders && focusBody { + return errors.New("Options -e (edit-headers) and -f (focus-body) are mutually exclusive") + } acct := app.SelectedAccount() if acct == nil { @@ -82,7 +88,7 @@ func (c Compose) Execute(args []string) error { composer, err := app.NewComposer(acct, acct.AccountConfig(), acct.Worker(), editHeaders, - c.Template, &headers, nil, msg.Body) + focusBody, c.Template, &headers, nil, msg.Body) if err != nil { return err } diff --git a/commands/account/recover.go b/commands/account/recover.go index 7f5b27f5..584cf6f2 100644 --- a/commands/account/recover.go +++ b/commands/account/recover.go @@ -67,10 +67,11 @@ func (r Recover) Execute(args []string) error { } editHeaders := (config.Compose.EditHeaders || r.Edit) && !r.NoEdit + focusBody := config.Compose.FocusBody composer, err := app.NewComposer(acct, acct.AccountConfig(), acct.Worker(), editHeaders, - "", nil, nil, bytes.NewReader(data)) + focusBody, "", nil, nil, bytes.NewReader(data)) if err != nil { return err } diff --git a/commands/msg/forward.go b/commands/msg/forward.go index 9b6e174b..405bf803 100644 --- a/commands/msg/forward.go +++ b/commands/msg/forward.go @@ -29,6 +29,7 @@ type forward struct { AttachFull bool `opt:"-F" desc:"Forward the full message as an RFC 2822 attachment."` Edit bool `opt:"-e" desc:"Force [compose].edit-headers = true."` NoEdit bool `opt:"-E" desc:"Force [compose].edit-headers = false."` + FocusBody bool `opt:"-b" desc:"Force [compose].focus-body = true."` Template string `opt:"-T" complete:"CompleteTemplate" desc:"Template name."` To []string `opt:"..." required:"false" complete:"CompleteTo" desc:"Recipient from address book."` } @@ -61,7 +62,13 @@ 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 + focusBody := config.Compose.FocusBody || f.FocusBody + + if editHeaders && focusBody { + return errors.New("Options -e (edit-headers) and -f (focus-body) are mutually exclusive") + } widget := app.SelectedTabContent().(app.ProvidesMessage) acct := widget.SelectedAccount() @@ -99,7 +106,7 @@ func (f forward) Execute(args []string) error { addTab := func() (*app.Composer, error) { composer, err := app.NewComposer(acct, acct.AccountConfig(), acct.Worker(), editHeaders, - f.Template, h, &original, nil) + focusBody, f.Template, h, &original, nil) if err != nil { app.PushError("Error: " + err.Error()) return nil, err diff --git a/commands/msg/invite.go b/commands/msg/invite.go index 36add098..cc3680c5 100644 --- a/commands/msg/invite.go +++ b/commands/msg/invite.go @@ -20,6 +20,7 @@ import ( type invite struct { Edit bool `opt:"-e" desc:"Force [compose].edit-headers = true."` NoEdit bool `opt:"-E" desc:"Force [compose].edit-headers = false."` + FocusBody bool `opt:"-f" desc:"Force [compose].focus-body = true."` SkipEditor bool `opt:"-s" desc:"Skip the editor and go directly to the review screen."` } @@ -57,8 +58,12 @@ func (i invite) Execute(args []string) error { if part == nil { return fmt.Errorf("no invitation found (missing text/calendar)") } - editHeaders := (config.Compose.EditHeaders || i.Edit) && !i.NoEdit + focusBody := config.Compose.FocusBody || i.FocusBody + + if editHeaders && focusBody { + return errors.New("Options -e (edit-headers) and -f (focus-body) are mutually exclusive") + } subject := trimLocalizedRe(msg.Envelope.Subject, acct.AccountConfig().LocalizedRe) switch args[0] { @@ -132,7 +137,7 @@ func (i invite) Execute(args []string) error { addTab := func(cr *calendar.Reply) error { composer, err := app.NewComposer(acct, acct.AccountConfig(), acct.Worker(), editHeaders, - "", h, &original, cr.PlainText) + focusBody, "", h, &original, cr.PlainText) if err != nil { app.PushError("Error: " + err.Error()) return err diff --git a/commands/msg/recall.go b/commands/msg/recall.go index 53a0de34..99e5b832 100644 --- a/commands/msg/recall.go +++ b/commands/msg/recall.go @@ -20,9 +20,10 @@ import ( ) type Recall struct { - Force bool `opt:"-f" desc:"Force recall if not in postpone directory."` - Edit bool `opt:"-e" desc:"Force [compose].edit-headers = true."` - NoEdit bool `opt:"-E" desc:"Force [compose].edit-headers = false."` + Force bool `opt:"-f" desc:"Force recall if not in postpone directory."` + Edit bool `opt:"-e" desc:"Force [compose].edit-headers = true."` + NoEdit bool `opt:"-E" desc:"Force [compose].edit-headers = false."` + FocusBody bool `opt:"-f" desc:"Force [compose].focus-body = true."`
There's a flag collision for the :recall command: -f is also used to force a recall. In the :forward command, you changed the FocusBody flag to -b; maybe that's a overall better flag name to use across all commands since -f is used in multiple places to mean "force" in some way? Alternatively and to avoid adding an additional flag to every command that invokes the composer, maybe your approach from v1 to set focusBody := (true | false) explicitly might be better? However, I would then make it opt-in, i.e. only change the default behavior when the user sets [composer].focus-body=true. What do you think?Personally I would prefer setting the focus-body option in config file only, because I see that just as an UX-related option "Where to set the focus when entering a new compose window" that I don't want to change during my aerc session via flags.I agree with this. Do not bother adding command flags to enable/disable it.So I would consume the config setting only (in every command) focusBody := config.Compose.FocusBody and additionally as in v2 I would give the hint that the flag -e and [compose].focus-body are mutually exclusive.No need to give any hint. Since edit-headers implies focus-body, you could have one or the other or both. It does not matter.In case you are thinking in the same way, I propose a v3 afterwards.Thanks again and have a great weekend!
} func init() { @@ -43,6 +44,11 @@ func (Recall) Aliases() []string { func (r Recall) Execute(args []string) error { editHeaders := (config.Compose.EditHeaders || r.Edit) && !r.NoEdit + focusBody := config.Compose.FocusBody || r.FocusBody + + if editHeaders && focusBody { + return errors.New("Options -e (edit-headers) and -f (focus-body) are mutually exclusive") + } widget := app.SelectedTabContent().(app.ProvidesMessage) acct := widget.SelectedAccount() @@ -112,7 +118,7 @@ func (r Recall) Execute(args []string) error { msg.FetchBodyPart(path, func(reader io.Reader) { composer, err := app.NewComposer(acct, acct.AccountConfig(), acct.Worker(), editHeaders, - "", msgInfo.RFC822Headers, nil, reader) + focusBody, "", msgInfo.RFC822Headers, nil, reader) if err != nil { app.PushError(err.Error()) return diff --git a/commands/msg/reply.go b/commands/msg/reply.go index 4cbd27c8..62848806 100644 --- a/commands/msg/reply.go +++ b/commands/msg/reply.go @@ -23,13 +23,14 @@ import ( ) type reply struct { - All bool `opt:"-a" desc:"Reply to all recipients."` - Close bool `opt:"-c" desc:"Close the view tab when replying."` - Quote bool `opt:"-q" desc:"Alias of -T quoted-reply."` - Template string `opt:"-T" complete:"CompleteTemplate" desc:"Template name."` - Edit bool `opt:"-e" desc:"Force [compose].edit-headers = true."` - NoEdit bool `opt:"-E" desc:"Force [compose].edit-headers = false."` - Account string `opt:"-A" complete:"CompleteAccount" desc:"Reply with the specified account."` + All bool `opt:"-a" desc:"Reply to all recipients."` + Close bool `opt:"-c" desc:"Close the view tab when replying."` + Quote bool `opt:"-q" desc:"Alias of -T quoted-reply."` + Template string `opt:"-T" complete:"CompleteTemplate" desc:"Template name."` + Edit bool `opt:"-e" desc:"Force [compose].edit-headers = true."` + NoEdit bool `opt:"-E" desc:"Force [compose].edit-headers = false."` + FocusBody bool `opt:"-f" desc:"Force [compose].focus-body = true."` + Account string `opt:"-A" complete:"CompleteAccount" desc:"Reply with the specified account."` } func init() { @@ -58,6 +59,11 @@ func (*reply) CompleteAccount(arg string) []string { func (r reply) Execute(args []string) error { editHeaders := (config.Compose.EditHeaders || r.Edit) && !r.NoEdit + focusBody := config.Compose.FocusBody || r.FocusBody + + if editHeaders && focusBody { + return errors.New("Options -e (edit-headers) and -f (focus-body) are mutually exclusive") + } widget := app.SelectedTabContent().(app.ProvidesMessage) @@ -181,7 +187,7 @@ func (r reply) Execute(args []string) error { addTab := func() error { composer, err := app.NewComposer(acct, acct.AccountConfig(), acct.Worker(), editHeaders, - r.Template, h, &original, nil) + focusBody, r.Template, h, &original, nil) if err != nil { app.PushError("Error: " + err.Error()) return err diff --git a/commands/msg/unsubscribe.go b/commands/msg/unsubscribe.go index 57299aa8..bd59caf6 100644 --- a/commands/msg/unsubscribe.go +++ b/commands/msg/unsubscribe.go @@ -19,8 +19,9 @@ import ( // Unsubscribe helps people unsubscribe from mailing lists by way of the // List-Unsubscribe header. type Unsubscribe struct { - Edit bool `opt:"-e" desc:"Force [compose].edit-headers = true."` - NoEdit bool `opt:"-E" desc:"Force [compose].edit-headers = false."` + Edit bool `opt:"-e" desc:"Force [compose].edit-headers = true."` + NoEdit bool `opt:"-E" desc:"Force [compose].edit-headers = false."` + FocusBody bool `opt:"-f" desc:"Force [compose].focus-body = true."` } func init() { @@ -43,6 +44,11 @@ func (Unsubscribe) Aliases() []string { // Execute runs the Unsubscribe command func (u Unsubscribe) Execute(args []string) error { editHeaders := (config.Compose.EditHeaders || u.Edit) && !u.NoEdit + focusBody := config.Compose.FocusBody || u.FocusBody + + if editHeaders && focusBody { + return errors.New("Options -e (edit-headers) and -f (focus-body) are mutually exclusive") + } widget := app.SelectedTabContent().(app.ProvidesMessage) msg, err := widget.SelectedMessage() @@ -68,7 +74,7 @@ func (u Unsubscribe) Execute(args []string) error { var err error switch strings.ToLower(method.Scheme) { case "mailto": - err = unsubscribeMailto(method, editHeaders) + err = unsubscribeMailto(method, editHeaders, focusBody) case "http", "https": err = unsubscribeHTTP(method) default: @@ -140,7 +146,7 @@ func parseUnsubscribeMethods(header string) (methods []*url.URL) { } } -func unsubscribeMailto(u *url.URL, editHeaders bool) error { +func unsubscribeMailto(u *url.URL, editHeaders bool, focusBody bool) error { widget := app.SelectedTabContent().(app.ProvidesMessage) acct := widget.SelectedAccount() if acct == nil { @@ -159,6 +165,7 @@ func unsubscribeMailto(u *url.URL, editHeaders bool) error { acct.AccountConfig(), acct.Worker(), editHeaders, + focusBody, "", h, nil, diff --git a/config/aerc.conf b/config/aerc.conf index 4a83625a..9070bcf3 100644 --- a/config/aerc.conf +++ b/config/aerc.conf @@ -654,6 +654,12 @@ # Default: false #edit-headers=false +# +# Sets focus to the email body when the composer window opens. +# +# Default: false +#focus-body=false + # # Specifies the command to be used to tab-complete email addresses. Any # occurrence of "%s" in the address-book-cmd will be replaced with what the diff --git a/config/compose.go b/config/compose.go index d6f25d31..c06c493c 100644 --- a/config/compose.go +++ b/config/compose.go @@ -1,6 +1,7 @@ package config import ( + "errors" "regexp" "git.sr.ht/~rjarry/aerc/lib/log" @@ -17,6 +18,7 @@ type ComposeConfig struct { FilePickerCmd string `ini:"file-picker-cmd"` FormatFlowed bool `ini:"format-flowed"` EditHeaders bool `ini:"edit-headers"` + FocusBody bool `ini:"focus-body" default:"false"` LFEditor bool `ini:"lf-editor"` } @@ -26,12 +28,16 @@ func parseCompose(file *ini.File) error { if err := MapToStruct(file.Section("compose"), Compose, true); err != nil { return err } + if Compose.EditHeaders && Compose.FocusBody { + return errors.New("[compose].edit-headers and [compose].focus-body are mutually exclusive") + } log.Debugf("aerc.conf: [compose] %#v", Compose) return nil } func (c *ComposeConfig) ParseLayout(sec *ini.Section, key *ini.Key) ([][]string, error) { layout := parseLayout(key.String()) + return layout, nil } diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd index 80922fa1..df660348 100644 --- a/doc/aerc-config.5.scd +++ b/doc/aerc-config.5.scd @@ -883,6 +883,12 @@ These options are configured in the *[compose]* section of _aerc.conf_. Default: _false_ +*focus-body* = _true_|_false_ + Sets focus to the email body when the composer window opens. This option and + *[compose].edit-headers* are mutually exclusive. + + Default: _false_ + *address-book-cmd* = _<command>_ Specifies the command to be used to tab-complete email addresses. Any occurrence of _%s_ in the *address-book-cmd* will be replaced with anything -- 2.47.0
builds.sr.ht <builds@sr.ht>aerc/patches: SUCCESS in 2m7s [composer: add focus-body option][0] v2 from [Markus Unkel][1] [0]: https://lists.sr.ht/~rjarry/aerc-devel/patches/55891 [1]: mailto:markus@unkel.io ✓ #1365601 SUCCESS aerc/patches/openbsd.yml https://builds.sr.ht/~rjarry/job/1365601 ✓ #1365600 SUCCESS aerc/patches/alpine-edge.yml https://builds.sr.ht/~rjarry/job/1365600
Hi Markus thanks for the fast v2 and the fix. :compose -f works now correctly.