~rjarry/aerc-devel

aerc: composer: add focus-body option v2 SUPERSEDED

Markus Unkel: 1
 composer: add focus-body option

 12 files changed, 90 insertions(+), 31 deletions(-)
#1365600 alpine-edge.yml success
#1365601 openbsd.yml success
Hi Markus,

Markus Unkel, Nov 09, 2024 at 14:16:
Next
Export patchset (mbox)
How do I use this?

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 -3
Learn more about email & git

[PATCH aerc v2] composer: add focus-body option Export this patch

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."`
}

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
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.