~sircmpwn/aerc

Rebase style configuration v4 PROPOSED

Kalyan Sriram: 3
 Rebase style configuration
 Rename selecter to selector
 Fix a couple of issues with the style patch.

 84 files changed, 1224 insertions(+), 478 deletions(-)
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/~sircmpwn/aerc/patches/11760/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH v4 1/3] Rebase style configuration Export this patch

Please see my previous email for comments, I don't want to type them out
again.

Thanks,
Kalyan
---
 Makefile                      |   7 +-
 commands/account/mkdir.go     |   2 +-
 commands/account/view.go      |   3 +-
 commands/compose/attach.go    |  10 +-
 commands/compose/detach.go    |   4 +-
 commands/compose/postpone.go  |   6 +-
 commands/compose/send.go      |   4 +-
 commands/exec.go              |  17 +-
 commands/msg/archive.go       |   2 +-
 commands/msg/copy.go          |   2 +-
 commands/msg/delete.go        |   6 +-
 commands/msg/forward.go       |   3 +-
 commands/msg/modify-labels.go |   2 +-
 commands/msg/move.go          |   2 +-
 commands/msg/pipe.go          |  18 +-
 commands/msg/read.go          |   2 +-
 commands/msg/recall.go        |   3 +-
 commands/msg/reply.go         |   3 +-
 commands/msgview/next.go      |   4 +-
 commands/msgview/open.go      |   6 +-
 commands/msgview/save.go      |   2 +-
 commands/term.go              |   3 +-
 commands/util.go              |   4 +-
 config/aerc.conf.in           |  11 +
 config/config.go              |  70 +++++-
 config/default_styleset       |  33 +++
 config/style.go               | 414 ++++++++++++++++++++++++++++++++++
 doc/aerc-config.5.scd         |  14 ++
 doc/aerc-stylesets.7.scd      | 189 ++++++++++++++++
 lib/ui/borders.go             |  13 +-
 lib/ui/stack.go               |  10 +-
 lib/ui/tab.go                 |  11 +-
 lib/ui/text.go                |  42 +---
 lib/ui/textinput.go           |  32 ++-
 widgets/account-wizard.go     | 109 +++++----
 widgets/account.go            |  10 +-
 widgets/aerc.go               |  28 ++-
 widgets/compose.go            |  69 +++---
 widgets/dirlist.go            |  10 +-
 widgets/exline.go             |   6 +-
 widgets/getpasswd.go          |  18 +-
 widgets/msglist.go            |  53 +++--
 widgets/msgviewer.go          |  63 ++++--
 widgets/pgpinfo.go            |  34 +--
 widgets/selecter.go           |  26 ++-
 widgets/spinner.go            |   6 +-
 widgets/status.go             |  49 ++--
 widgets/tabhost.go            |   3 +
 48 files changed, 1124 insertions(+), 314 deletions(-)
 create mode 100644 config/default_styleset
 create mode 100644 config/style.go
 create mode 100644 doc/aerc-stylesets.7.scd

diff --git a/Makefile b/Makefile
index 1e7fbd6..1c1de75 100644
--- a/Makefile
+++ b/Makefile
@@ -35,7 +35,8 @@ DOCS := \
	aerc-notmuch.5 \
	aerc-smtp.5 \
	aerc-tutorial.7 \
	aerc-templates.7
	aerc-templates.7 \
	aerc-stylesets.7

.1.scd.1:
	scdoc < $< > $@
@@ -58,7 +59,7 @@ clean:

install: all
	mkdir -m755 -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 $(DESTDIR)$(MANDIR)/man5 $(DESTDIR)$(MANDIR)/man7 \
		$(DESTDIR)$(SHAREDIR) $(DESTDIR)$(SHAREDIR)/filters $(DESTDIR)$(SHAREDIR)/templates
		$(DESTDIR)$(SHAREDIR) $(DESTDIR)$(SHAREDIR)/filters $(DESTDIR)$(SHAREDIR)/templates $(DESTDIR)$(SHAREDIR)/stylesets
	install -m755 aerc $(DESTDIR)$(BINDIR)/aerc
	install -m644 aerc.1 $(DESTDIR)$(MANDIR)/man1/aerc.1
	install -m644 aerc-search.1 $(DESTDIR)$(MANDIR)/man1/aerc-search.1
@@ -70,6 +71,7 @@ install: all
	install -m644 aerc-smtp.5 $(DESTDIR)$(MANDIR)/man5/aerc-smtp.5
	install -m644 aerc-tutorial.7 $(DESTDIR)$(MANDIR)/man7/aerc-tutorial.7
	install -m644 aerc-templates.7 $(DESTDIR)$(MANDIR)/man7/aerc-templates.7
	install -m644 aerc-stylesets.7 $(DESTDIR)$(MANDIR)/man7/aerc-stylesets.7
	install -m644 config/accounts.conf $(DESTDIR)$(SHAREDIR)/accounts.conf
	install -m644 aerc.conf $(DESTDIR)$(SHAREDIR)/aerc.conf
	install -m644 config/binds.conf $(DESTDIR)$(SHAREDIR)/binds.conf
@@ -78,6 +80,7 @@ install: all
	install -m755 filters/plaintext $(DESTDIR)$(SHAREDIR)/filters/plaintext
	install -m644 templates/quoted_reply $(DESTDIR)$(SHAREDIR)/templates/quoted_reply
	install -m644 templates/forward_as_body $(DESTDIR)$(SHAREDIR)/templates/forward_as_body
	install -m644 config/default_styleset $(DESTDIR)$(SHAREDIR)/stylesets/default

RMDIR_IF_EMPTY:=sh -c '\
if test -d $$0 && ! ls -1qA $$0 | grep -q . ; then \
diff --git a/commands/account/mkdir.go b/commands/account/mkdir.go
index 4352a42..f99fc01 100644
--- a/commands/account/mkdir.go
+++ b/commands/account/mkdir.go
@@ -40,7 +40,7 @@ func (MakeDir) Execute(aerc *widgets.Aerc, args []string) error {
			aerc.PushStatus("Directory created.", 10*time.Second)
			acct.Directories().Select(name)
		case *types.Error:
			aerc.PushError(" " + msg.Error.Error())
			aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
		}
	})
	return nil
diff --git a/commands/account/view.go b/commands/account/view.go
index b421666..d4653be 100644
--- a/commands/account/view.go
+++ b/commands/account/view.go
@@ -2,6 +2,7 @@ package account

import (
	"errors"
	"time"

	"git.sr.ht/~sircmpwn/aerc/lib"
	"git.sr.ht/~sircmpwn/aerc/widgets"
@@ -41,7 +42,7 @@ func (ViewMessage) Execute(aerc *widgets.Aerc, args []string) error {
	lib.NewMessageStoreView(msg, store, aerc.DecryptKeys,
		func(view lib.MessageView, err error) {
			if err != nil {
				aerc.PushError(err.Error())
				aerc.PushError(err.Error(), 10*time.Second)
				return
			}
			viewer := widgets.NewMessageViewer(acct, aerc.Config(), view)
diff --git a/commands/compose/attach.go b/commands/compose/attach.go
index 2b633dc..6b8d72f 100644
--- a/commands/compose/attach.go
+++ b/commands/compose/attach.go
@@ -8,7 +8,6 @@ import (

	"git.sr.ht/~sircmpwn/aerc/commands"
	"git.sr.ht/~sircmpwn/aerc/widgets"
	"github.com/gdamore/tcell"
	"github.com/mitchellh/go-homedir"
)

@@ -36,24 +35,23 @@ func (Attach) Execute(aerc *widgets.Aerc, args []string) error {

	path, err := homedir.Expand(path)
	if err != nil {
		aerc.PushError(" " + err.Error())
		aerc.PushError(" "+err.Error(), 10*time.Second)
		return err
	}

	pathinfo, err := os.Stat(path)
	if err != nil {
		aerc.PushError(" " + err.Error())
		aerc.PushError(" "+err.Error(), 10*time.Second)
		return err
	} else if pathinfo.IsDir() {
		aerc.PushError("Attachment must be a file, not a directory")
		aerc.PushError("Attachment must be a file, not a directory", 10*time.Second)
		return nil
	}

	composer, _ := aerc.SelectedTab().(*widgets.Composer)
	composer.AddAttachment(path)

	aerc.PushStatus(fmt.Sprintf("Attached %s", pathinfo.Name()), 10*time.Second).
		Color(tcell.ColorDefault, tcell.ColorGreen)
	aerc.PushSuccess(fmt.Sprintf("Attached %s", pathinfo.Name()), 10*time.Second)

	return nil
}
diff --git a/commands/compose/detach.go b/commands/compose/detach.go
index e8b07ed..8bc0e88 100644
--- a/commands/compose/detach.go
+++ b/commands/compose/detach.go
@@ -6,7 +6,6 @@ import (
	"time"

	"git.sr.ht/~sircmpwn/aerc/widgets"
	"github.com/gdamore/tcell"
)

type Detach struct{}
@@ -44,8 +43,7 @@ func (Detach) Execute(aerc *widgets.Aerc, args []string) error {
		return err
	}

	aerc.PushStatus(fmt.Sprintf("Detached %s", path), 10*time.Second).
		Color(tcell.ColorDefault, tcell.ColorGreen)
	aerc.PushSuccess(fmt.Sprintf("Detached %s", path), 10*time.Second)

	return nil
}
diff --git a/commands/compose/postpone.go b/commands/compose/postpone.go
index 60c9df1..90b6134 100644
--- a/commands/compose/postpone.go
+++ b/commands/compose/postpone.go
@@ -63,7 +63,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error {
	go func() {
		errStr := <-errChan
		if errStr != "" {
			aerc.PushError(" " + errStr)
			aerc.PushError(" "+errStr, 10*time.Second)
			return
		}

@@ -71,7 +71,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error {
		ctr := datacounter.NewWriterCounter(ioutil.Discard)
		err = composer.WriteMessage(header, ctr)
		if err != nil {
			aerc.PushError(errors.Wrap(err, "WriteMessage").Error())
			aerc.PushError(errors.Wrap(err, "WriteMessage").Error(), 10*time.Second)
			composer.Close()
			return
		}
@@ -90,7 +90,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error {
				r.Close()
				composer.Close()
			case *types.Error:
				aerc.PushError(" " + msg.Error.Error())
				aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
				r.Close()
				composer.Close()
			}
diff --git a/commands/compose/send.go b/commands/compose/send.go
index 40ac4ca..cbcc876 100644
--- a/commands/compose/send.go
+++ b/commands/compose/send.go
@@ -224,7 +224,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
		aerc.PushStatus("Sending...", 10*time.Second)
		nbytes, err := sendAsync()
		if err != nil {
			aerc.PushError(" " + err.Error())
			aerc.PushError(" " + err.Error(), 10*time.Second)
			return
		}
		if config.CopyTo != "" {
@@ -245,7 +245,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
					composer.SetSent()
					composer.Close()
				case *types.Error:
					aerc.PushError(" " + msg.Error.Error())
					aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
					r.Close()
					composer.Close()
				}
diff --git a/commands/exec.go b/commands/exec.go
index e15afbe..0a5470d 100644
--- a/commands/exec.go
+++ b/commands/exec.go
@@ -7,8 +7,6 @@ import (
	"time"

	"git.sr.ht/~sircmpwn/aerc/widgets"

	"github.com/gdamore/tcell"
)

type ExecCmd struct{}
@@ -33,16 +31,17 @@ func (ExecCmd) Execute(aerc *widgets.Aerc, args []string) error {
	go func() {
		err := cmd.Run()
		if err != nil {
			aerc.PushError(" " + err.Error())
			aerc.PushError(" "+err.Error(), 10*time.Second)
		} else {
			color := tcell.ColorDefault
			if cmd.ProcessState.ExitCode() != 0 {
				color = tcell.ColorRed
				aerc.PushError(fmt.Sprintf(
					"%s: completed with status %d", args[0],
					cmd.ProcessState.ExitCode()), 10*time.Second)
			} else {
				aerc.PushStatus(fmt.Sprintf(
					"%s: completed with status %d", args[0],
					cmd.ProcessState.ExitCode()), 10*time.Second)
			}
			aerc.PushStatus(fmt.Sprintf(
				"%s: completed with status %d", args[0],
				cmd.ProcessState.ExitCode()), 10*time.Second).
				Color(tcell.ColorDefault, color)
		}
	}()
	return nil
diff --git a/commands/msg/archive.go b/commands/msg/archive.go
index 07de13f..8d42308 100644
--- a/commands/msg/archive.go
+++ b/commands/msg/archive.go
@@ -86,7 +86,7 @@ func (Archive) Execute(aerc *widgets.Aerc, args []string) error {
			case *types.Done:
				wg.Done()
			case *types.Error:
				aerc.PushError(" " + msg.Error.Error())
				aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
				success = false
				wg.Done()
			}
diff --git a/commands/msg/copy.go b/commands/msg/copy.go
index f3d4030..e822c5c 100644
--- a/commands/msg/copy.go
+++ b/commands/msg/copy.go
@@ -60,7 +60,7 @@ func (Copy) Execute(aerc *widgets.Aerc, args []string) error {
			case *types.Done:
				aerc.PushStatus("Messages copied.", 10*time.Second)
			case *types.Error:
				aerc.PushError(" " + msg.Error.Error())
				aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
			}
		})
	return nil
diff --git a/commands/msg/delete.go b/commands/msg/delete.go
index 6eb35eb..26f0df8 100644
--- a/commands/msg/delete.go
+++ b/commands/msg/delete.go
@@ -47,10 +47,10 @@ func (Delete) Execute(aerc *widgets.Aerc, args []string) error {
		case *types.Done:
			aerc.PushStatus("Messages deleted.", 10*time.Second)
		case *types.Error:
			aerc.PushError(" " + msg.Error.Error())
			aerc.PushError(" " + msg.Error.Error(), 10*time.Second)
		case *types.Unsupported:
			// notmuch doesn't support it, we want the user to know
			aerc.PushError(" error, unsupported for this worker")
			aerc.PushError(" error, unsupported for this worker", 10*time.Second)
		}
	})

@@ -71,7 +71,7 @@ func (Delete) Execute(aerc *widgets.Aerc, args []string) error {
			lib.NewMessageStoreView(next, store, aerc.DecryptKeys,
				func(view lib.MessageView, err error) {
					if err != nil {
						aerc.PushError(err.Error())
						aerc.PushError(err.Error(), 10*time.Second)
						return
					}
					nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view)
diff --git a/commands/msg/forward.go b/commands/msg/forward.go
index 5f4da5c..61e3a24 100644
--- a/commands/msg/forward.go
+++ b/commands/msg/forward.go
@@ -9,6 +9,7 @@ import (
	"os"
	"path"
	"strings"
	"time"

	"git.sr.ht/~sircmpwn/aerc/models"
	"git.sr.ht/~sircmpwn/aerc/widgets"
@@ -83,7 +84,7 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
		composer, err := widgets.NewComposer(aerc, acct, aerc.Config(), acct.AccountConfig(),
			acct.Worker(), template, defaults, original)
		if err != nil {
			aerc.PushError("Error: " + err.Error())
			aerc.PushError("Error: "+err.Error(), 10*time.Second)
			return nil, err
		}

diff --git a/commands/msg/modify-labels.go b/commands/msg/modify-labels.go
index f91075a..d74aece 100644
--- a/commands/msg/modify-labels.go
+++ b/commands/msg/modify-labels.go
@@ -58,7 +58,7 @@ func (ModifyLabels) Execute(aerc *widgets.Aerc, args []string) error {
		case *types.Done:
			aerc.PushStatus("labels updated", 10*time.Second)
		case *types.Error:
			aerc.PushError(" " + msg.Error.Error())
			aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
		}
	})
	return nil
diff --git a/commands/msg/move.go b/commands/msg/move.go
index 41f61da..1a8f949 100644
--- a/commands/msg/move.go
+++ b/commands/msg/move.go
@@ -71,7 +71,7 @@ func (Move) Execute(aerc *widgets.Aerc, args []string) error {
		case *types.Done:
			aerc.PushStatus("Message moved to "+joinedArgs, 10*time.Second)
		case *types.Error:
			aerc.PushError(" " + msg.Error.Error())
			aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
		}
	})
	return nil
diff --git a/commands/msg/pipe.go b/commands/msg/pipe.go
index 20cb8b4..5a0b120 100644
--- a/commands/msg/pipe.go
+++ b/commands/msg/pipe.go
@@ -12,7 +12,6 @@ import (
	"git.sr.ht/~sircmpwn/aerc/worker/types"

	"git.sr.ht/~sircmpwn/getopt"
	"github.com/gdamore/tcell"
)

type Pipe struct{}
@@ -76,7 +75,7 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error {
	doTerm := func(reader io.Reader, name string) {
		term, err := commands.QuickTerm(aerc, cmd, reader)
		if err != nil {
			aerc.PushError(" " + err.Error())
			aerc.PushError(" "+err.Error(), 10*time.Second)
			return
		}
		aerc.NewTab(term, name)
@@ -94,16 +93,17 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error {
		}()
		err = ecmd.Run()
		if err != nil {
			aerc.PushError(" " + err.Error())
			aerc.PushError(" "+err.Error(), 10*time.Second)
		} else {
			color := tcell.ColorDefault
			if ecmd.ProcessState.ExitCode() != 0 {
				color = tcell.ColorRed
				aerc.PushError(fmt.Sprintf(
					"%s: completed with status %d", cmd[0],
					ecmd.ProcessState.ExitCode()), 10*time.Second)
			} else {
				aerc.PushStatus(fmt.Sprintf(
					"%s: completed with status %d", cmd[0],
					ecmd.ProcessState.ExitCode()), 10*time.Second)
			}
			aerc.PushStatus(fmt.Sprintf(
				"%s: completed with status %d", cmd[0],
				ecmd.ProcessState.ExitCode()), 10*time.Second).
				Color(tcell.ColorDefault, color)
		}
	}

diff --git a/commands/msg/read.go b/commands/msg/read.go
index 325b776..a0b68bc 100644
--- a/commands/msg/read.go
+++ b/commands/msg/read.go
@@ -187,7 +187,7 @@ func submitFlagChange(aerc *widgets.Aerc, store *lib.MessageStore,
		case *types.Done:
			wg.Done()
		case *types.Error:
			aerc.PushError(" " + msg.Error.Error())
			aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
			*success = false
			wg.Done()
		}
diff --git a/commands/msg/recall.go b/commands/msg/recall.go
index 7c9ac19..6c5e973 100644
--- a/commands/msg/recall.go
+++ b/commands/msg/recall.go
@@ -2,6 +2,7 @@ package msg

import (
	"io"
	"time"

	"github.com/emersion/go-message"
	_ "github.com/emersion/go-message/charset"
@@ -91,7 +92,7 @@ func (Recall) Execute(aerc *widgets.Aerc, args []string) error {
			}, func(msg types.WorkerMessage) {
				switch msg := msg.(type) {
				case *types.Error:
					aerc.PushError(" " + msg.Error.Error())
					aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
					composer.Close()
				}
			})
diff --git a/commands/msg/reply.go b/commands/msg/reply.go
index 1deab31..1655052 100644
--- a/commands/msg/reply.go
+++ b/commands/msg/reply.go
@@ -7,6 +7,7 @@ import (
	"io"
	gomail "net/mail"
	"strings"
	"time"

	"git.sr.ht/~sircmpwn/getopt"

@@ -139,7 +140,7 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {
		composer, err := widgets.NewComposer(aerc, acct, aerc.Config(),
			acct.AccountConfig(), acct.Worker(), template, defaults, original)
		if err != nil {
			aerc.PushError("Error: " + err.Error())
			aerc.PushError("Error: "+err.Error(), 10*time.Second)
			return err
		}

diff --git a/commands/msgview/next.go b/commands/msgview/next.go
index 978cf10..f9fb3d7 100644
--- a/commands/msgview/next.go
+++ b/commands/msgview/next.go
@@ -1,6 +1,8 @@
package msgview

import (
	"time"

	"git.sr.ht/~sircmpwn/aerc/commands/account"
	"git.sr.ht/~sircmpwn/aerc/lib"
	"git.sr.ht/~sircmpwn/aerc/widgets"
@@ -40,7 +42,7 @@ func (NextPrevMsg) Execute(aerc *widgets.Aerc, args []string) error {
	lib.NewMessageStoreView(nextMsg, store, aerc.DecryptKeys,
		func(view lib.MessageView, err error) {
			if err != nil {
				aerc.PushError(err.Error())
				aerc.PushError(err.Error(), 10*time.Second)
				return
			}
			nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view)
diff --git a/commands/msgview/open.go b/commands/msgview/open.go
index 4aa6133..b4dfab7 100644
--- a/commands/msgview/open.go
+++ b/commands/msgview/open.go
@@ -49,19 +49,19 @@ func (Open) Execute(aerc *widgets.Aerc, args []string) error {

		tmpFile, err := ioutil.TempFile(os.TempDir(), "aerc-*"+extension)
		if err != nil {
			aerc.PushError(" " + err.Error())
			aerc.PushError(" "+err.Error(), 10*time.Second)
			return
		}
		defer tmpFile.Close()

		_, err = io.Copy(tmpFile, reader)
		if err != nil {
			aerc.PushError(" " + err.Error())
			aerc.PushError(" "+err.Error(), 10*time.Second)
			return
		}

		lib.OpenFile(tmpFile.Name(), func(err error) {
			aerc.PushError(" " + err.Error())
			aerc.PushError(" " + err.Error(), 10*time.Second)
		})

		aerc.PushStatus("Opened", 10*time.Second)
diff --git a/commands/msgview/save.go b/commands/msgview/save.go
index ef6bba8..ea1b8f3 100644
--- a/commands/msgview/save.go
+++ b/commands/msgview/save.go
@@ -128,7 +128,7 @@ func (Save) Execute(aerc *widgets.Aerc, args []string) error {
	go func() {
		err := <-ch
		if err != nil {
			aerc.PushError(fmt.Sprintf("Save failed: %v", err))
			aerc.PushError(fmt.Sprintf("Save failed: %v", err), 10*time.Second)
			return
		}
		aerc.PushStatus("Saved to "+path, 10*time.Second)
diff --git a/commands/term.go b/commands/term.go
index 00f6937..9023285 100644
--- a/commands/term.go
+++ b/commands/term.go
@@ -2,6 +2,7 @@ package commands

import (
	"os/exec"
	"time"

	"github.com/riywo/loginshell"

@@ -46,7 +47,7 @@ func TermCore(aerc *widgets.Aerc, args []string) error {
	term.OnClose = func(err error) {
		aerc.RemoveTab(term)
		if err != nil {
			aerc.PushError(" " + err.Error())
			aerc.PushError(" "+err.Error(), 10*time.Second)
		}
	}
	return nil
diff --git a/commands/util.go b/commands/util.go
index fdf20bd..7c7b6ab 100644
--- a/commands/util.go
+++ b/commands/util.go
@@ -32,7 +32,7 @@ func QuickTerm(aerc *widgets.Aerc, args []string, stdin io.Reader) (*widgets.Ter

	term.OnClose = func(err error) {
		if err != nil {
			aerc.PushError(" " + err.Error())
			aerc.PushError(" "+err.Error(), 10*time.Second)
			// remove the tab on error, otherwise it gets stuck
			aerc.RemoveTab(term)
		} else {
@@ -56,7 +56,7 @@ func QuickTerm(aerc *widgets.Aerc, args []string, stdin io.Reader) (*widgets.Ter

		err := <-status
		if err != nil {
			aerc.PushError(" " + err.Error())
			aerc.PushError(" "+err.Error(), 10*time.Second)
		}
	}

diff --git a/config/aerc.conf.in b/config/aerc.conf.in
index 3348efa..b9381a8 100644
--- a/config/aerc.conf.in
+++ b/config/aerc.conf.in
@@ -67,6 +67,17 @@ sort=
# Default: true
next-message-on-delete=true

# The directories where the stylesets are stored. It takes a colon-separated
# list of directories.
#
# default: @SHAREDIR@/stylesets/
stylesets-dirs=@SHAREDIR@/stylesets/

# Sets the styleset to use for the aerc ui elements.
#
# Default: default
styleset-name=default

[viewer]
#
# Specifies the pager to use when displaying emails. Note that some filters
diff --git a/config/config.go b/config/config.go
index 00a52ce..0ccf350 100644
--- a/config/config.go
+++ b/config/config.go
@@ -45,6 +45,9 @@ type UIConfig struct {
	NextMessageOnDelete bool          `ini:"next-message-on-delete"`
	CompletionDelay     time.Duration `ini:"completion-delay"`
	CompletionPopovers  bool          `ini:"completion-popovers"`
	StyleSetDirs        []string      `ini:"stylesets-dirs", delim:":"`
	StyleSetName        string        `ini:"styleset-name"`
	style               StyleSet
}

type ContextType int
@@ -337,6 +340,11 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
		if err := ui.MapTo(&config.Ui); err != nil {
			return err
		}

		stylesetsDirs := ui.Key("stylesets-dirs").String()
		if stylesetsDirs != "" {
			config.Ui.StyleSetDirs = strings.Split(stylesetsDirs, ":")
		}
	}
	for _, sectionName := range file.SectionStrings() {
		if !strings.Contains(sectionName, "ui:") {
@@ -351,6 +359,10 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
		if err := uiSection.MapTo(&uiSubConfig); err != nil {
			return err
		}
		stylesetsDirs := uiSection.Key("stylesets-dirs").String()
		if stylesetsDirs != "" {
			uiSubConfig.StyleSetDirs = strings.Split(stylesetsDirs, ":")
		}
		contextualUi :=
			UIConfigContext{
				UiConfig: uiSubConfig,
@@ -411,6 +423,19 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
			}
		}
	}

	if err := config.Ui.loadStyleSet(
		config.Ui.StyleSetDirs); err != nil {
		return err
	}

	for idx, _ := range config.ContextualUis {
		if err := config.ContextualUis[idx].UiConfig.loadStyleSet(
			config.Ui.StyleSetDirs); err != nil {
			return err
		}
	}

	return nil
}

@@ -471,6 +496,8 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
			NextMessageOnDelete: true,
			CompletionDelay:     250 * time.Millisecond,
			CompletionPopovers:  true,
			StyleSetDirs:        []string{path.Join(sharedir, "stylesets")},
			StyleSetName:        "default",
		},

		ContextualUis: []UIConfigContext{},
@@ -500,6 +527,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
			Forwards:     "forward_as_body",
		},
	}

	// These bindings are not configurable
	config.Bindings.AccountWizard.ExKey = KeyStroke{
		Key: tcell.KeyCtrlE,
@@ -510,6 +538,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
	if err = config.LoadConfig(file); err != nil {
		return nil, err
	}

	if ui, err := file.GetSection("general"); err == nil {
		if err := ui.MapTo(&config.General); err != nil {
			return nil, err
@@ -617,8 +646,24 @@ func parseLayout(layout string) [][]string {
	return l
}

func (config *AercConfig) mergeContextualUi(baseUi *UIConfig,
	contextType ContextType, s string) {
func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error {
	ui.style = NewStyleSet()
    err := ui.style.LoadStyleSet(ui.StyleSetName, styleSetDirs)
    if err != nil {
        fmt.Errorf("Error while parsing styleset \"%s\": %s", ui.StyleSetName, err)
        fmt.Printf("Loading default styles")

        err = ui.style.LoadDefaultStyleSet()
        if err != nil {
            return fmt.Errorf("Error while loading default styleset: %s", err)
        }
    }

	return nil
}

func (config AercConfig) mergeContextualUi(baseUi UIConfig,
	contextType ContextType, s string) UIConfig {
	for _, contextualUi := range config.ContextualUis {
		if contextualUi.ContextType != contextType {
			continue
@@ -628,17 +673,30 @@ func (config *AercConfig) mergeContextualUi(baseUi *UIConfig,
			continue
		}

		mergo.MergeWithOverwrite(baseUi, contextualUi.UiConfig)
		return
		mergo.Merge(&baseUi, contextualUi.UiConfig, mergo.WithOverride)
		if contextualUi.UiConfig.StyleSetName != "" {
			baseUi.style = contextualUi.UiConfig.style
		}
		return baseUi
	}

	return baseUi
}

func (config *AercConfig) GetUiConfig(params map[ContextType]string) UIConfig {
func (config AercConfig) GetUiConfig(params map[ContextType]string) UIConfig {
	baseUi := config.Ui

	for k, v := range params {
		config.mergeContextualUi(&baseUi, k, v)
		baseUi = config.mergeContextualUi(baseUi, k, v)
	}

	return baseUi
}

func (uiConfig UIConfig) GetStyle(so StyleObject) tcell.Style {
	return uiConfig.style.Get(so)
}

func (uiConfig UIConfig) GetStyleSelected(so StyleObject) tcell.Style {
	return uiConfig.style.Selected(so)
}
diff --git a/config/default_styleset b/config/default_styleset
new file mode 100644
index 0000000..fa52f23
--- /dev/null
+++ b/config/default_styleset
@@ -0,0 +1,33 @@
# 
# aerc default styleset
# 
# This styleset uses the terminal defaults as its base.
# More information on how to configure the styleset can be found in
# the *aerc-styleset.7* manpage. Please read the manual before
# modifying or creating a styleset.

*.default=true
*.selected.reverse=toggle

title.reverse=true
header.bold=true

*error.bold=true
error.fg=red
warning.fg=yellow
success.fg=green

statusline*.default=true
statusline_default.reverse=true
statusline_error.fg=red
statusline_error.reverse=true

msglist_unread.bold=true

completion_pill.reverse=true

tab.reverse=true
border.reverse = true

selector_focused.reverse=true
selector_chooser.bold=true
diff --git a/config/style.go b/config/style.go
new file mode 100644
index 0000000..dfedf35
--- /dev/null
+++ b/config/style.go
@@ -0,0 +1,414 @@
package config

import (
	"errors"
	"os"
	"path"
	"regexp"
	"strings"

	"github.com/gdamore/tcell"
	"github.com/go-ini/ini"
	"github.com/mitchellh/go-homedir"
)

type StyleObject int32

const (
	STYLE_DEFAULT StyleObject = iota
	STYLE_ERROR
	STYLE_WARNING
	STYLE_SUCCESS

	STYLE_TITLE
	STYLE_HEADER

	STYLE_STATUSLINE_DEFAULT
	STYLE_STATUSLINE_ERROR
	STYLE_STATUSLINE_SUCCESS

	STYLE_MSGLIST_DEFAULT
	STYLE_MSGLIST_UNREAD
	STYLE_MSGLIST_READ
	STYLE_MSGLIST_DELETED
	STYLE_MSGLIST_MARKED
	STYLE_MSGLIST_FLAGGED

	STYLE_DIRLIST_DEFAULT

	STYLE_COMPLETION_DEFAULT
	STYLE_COMPLETION_GUTTER
	STYLE_COMPLETION_PILL

	STYLE_TAB
	STYLE_STACK
	STYLE_SPINNER
	STYLE_BORDER

	STYLE_SELECTOR_DEFAULT
	STYLE_SELECTOR_FOCUSED
	STYLE_SELECTOR_CHOOSER
)

var StyleNames = map[string]StyleObject{
	"default": STYLE_DEFAULT,
	"error":   STYLE_ERROR,
	"warning": STYLE_WARNING,
	"success": STYLE_SUCCESS,

	"title":  STYLE_TITLE,
	"header": STYLE_HEADER,

	"statusline_default": STYLE_STATUSLINE_DEFAULT,
	"statusline_error":   STYLE_STATUSLINE_ERROR,
	"statusline_success": STYLE_STATUSLINE_SUCCESS,

	"msglist_default": STYLE_MSGLIST_DEFAULT,
	"msglist_unread":  STYLE_MSGLIST_UNREAD,
	"msglist_read":    STYLE_MSGLIST_READ,
	"msglist_deleted": STYLE_MSGLIST_DELETED,
	"msglist_marked":  STYLE_MSGLIST_MARKED,
	"msglist_flagged": STYLE_MSGLIST_FLAGGED,

	"dirlist_default": STYLE_DIRLIST_DEFAULT,

	"completion_default": STYLE_COMPLETION_DEFAULT,
	"completion_gutter":  STYLE_COMPLETION_GUTTER,
	"completion_pill":    STYLE_COMPLETION_PILL,

	"tab":     STYLE_TAB,
	"stack":   STYLE_STACK,
	"spinner": STYLE_SPINNER,
	"border":  STYLE_BORDER,

	"selector_default": STYLE_SELECTOR_DEFAULT,
	"selector_focused": STYLE_SELECTOR_FOCUSED,
	"selector_chooser": STYLE_SELECTOR_CHOOSER,
}

type Style struct {
	Fg        tcell.Color
	Bg        tcell.Color
	Bold      bool
	Blink     bool
	Underline bool
	Reverse   bool
}

var StyleDefaults = []byte(`
*.default=true
*.selected.reverse=toggle

title.reverse=true
header.bold=true

*error.bold=true
error.fg=red
warning.fg=yello
success.fg=green

statusline*.default=true
statusline_default.reverse=true
statusline_error.fg=red
statusline_error.reverse=true

msglist_unread.bold=true

completion_pill.reverse=true

tab.reverse=true
border.reverse=true

selector_focused.reverse=true
selector_chooser.bold=true
`)

func (s Style) Get() tcell.Style {
	return tcell.StyleDefault.
		Foreground(s.Fg).
		Background(s.Bg).
		Bold(s.Bold).
		Blink(s.Blink).
		Underline(s.Blink).
		Reverse(s.Reverse)
}

func (s *Style) Normal() {
	s.Bold = false
	s.Blink = false
	s.Underline = false
	s.Reverse = false
}

func (s *Style) Default() *Style {
	s.Fg = tcell.ColorDefault
	s.Bg = tcell.ColorDefault
	return s
}

func (s *Style) Reset() *Style {
	s.Default()
	s.Normal()
	return s
}

func boolSwitch(val string, cur_val bool) (bool, error) {
	switch val {
	case "true":
		return true, nil
	case "false":
		return false, nil
	case "toggle":
		return !cur_val, nil
	default:
		return cur_val, errors.New(
			"Bool Switch attribute must be true, false, or toggle")
	}
}

func (s *Style) Set(attr, val string) error {
	switch attr {
	case "fg":
		s.Fg = tcell.GetColor(val)
	case "bg":
		s.Bg = tcell.GetColor(val)
	case "bold":
		if state, err := boolSwitch(val, s.Bold); err != nil {
			return err
		} else {
			s.Bold = state
		}
	case "blink":
		if state, err := boolSwitch(val, s.Blink); err != nil {
			return err
		} else {
			s.Blink = state
		}
	case "underline":
		if state, err := boolSwitch(val, s.Underline); err != nil {
			return err
		} else {
			s.Underline = state
		}
	case "reverse":
		if state, err := boolSwitch(val, s.Reverse); err != nil {
			return err
		} else {
			s.Reverse = state
		}
	case "default":
		s.Default()
	case "normal":
		s.Normal()
	default:
		return errors.New("Unknown style attribute: " + attr)
	}

	return nil
}

type StyleSet struct {
	objects  map[StyleObject]*Style
	selected map[StyleObject]*Style
}

func NewStyleSet() StyleSet {
	ss := StyleSet{
		objects:  make(map[StyleObject]*Style),
		selected: make(map[StyleObject]*Style),
	}
	for _, so := range StyleNames {
		ss.objects[so] = new(Style)
		ss.selected[so] = new(Style)
	}

	return ss
}

func (ss StyleSet) reset() {
	for _, so := range StyleNames {
		ss.objects[so].Reset()
		ss.selected[so].Reset()
	}
}

func (ss StyleSet) Get(so StyleObject) tcell.Style {
	return ss.objects[so].Get()
}

func (ss StyleSet) Selected(so StyleObject) tcell.Style {
	return ss.selected[so].Get()
}

func findStyleSet(stylesetName string, stylesetsDir []string) (string, error) {
	for _, dir := range stylesetsDir {
		stylesetPath, err := homedir.Expand(path.Join(dir, stylesetName))
		if err != nil {
			return "", err
		}

		if _, err := os.Stat(stylesetPath); os.IsNotExist(err) {
			continue
		}

		return stylesetPath, nil
	}

	return "", errors.New("Can't find styleset - " + stylesetName)
}

func (ss *StyleSet) ParseStyleSet(file *ini.File) error {
	ss.reset()

	defaultSection, err := file.GetSection(ini.DefaultSection)
	if err != nil {
		return err
	}

	selectedKeys := []string{}

	for _, key := range defaultSection.KeyStrings() {
		tokens := strings.Split(key, ".")
		var styleName, attr string
		switch len(tokens) {
		case 2:
			styleName, attr = tokens[0], tokens[1]
		case 3:
			if tokens[1] != "selected" {
				return errors.New("Unknown modifier: " + tokens[1])
			}
			selectedKeys = append(selectedKeys, key)
			continue
		default:
			return errors.New("Style parsing error: " + key)
		}
		val := defaultSection.KeysHash()[key]

		if strings.ContainsAny(styleName, "*?") {
			regex := fnmatchToRegex(styleName)
			for sn, so := range StyleNames {
				matched, err := regexp.MatchString(regex, sn)
				if err != nil {
					return err
				}

				if !matched {
					continue
				}

				if err := ss.objects[so].Set(attr, val); err != nil {
					return err
				}
				if err := ss.selected[so].Set(attr, val); err != nil {
					return err
				}
			}
		} else {
			so, ok := StyleNames[styleName]
			if !ok {
				return errors.New("Unknown style object: " + styleName)
			}
			if err := ss.objects[so].Set(attr, val); err != nil {
				return err
			}
			if err := ss.selected[so].Set(attr, val); err != nil {
				return err
			}
		}
	}

	for _, key := range selectedKeys {
		tokens := strings.Split(key, ".")
		styleName, modifier, attr := tokens[0], tokens[1], tokens[2]
		if modifier != "selected" {
			return errors.New("Unknown modifier: " + modifier)
		}

		val := defaultSection.KeysHash()[key]

		if strings.ContainsAny(styleName, "*?") {
			regex := fnmatchToRegex(styleName)
			for sn, so := range StyleNames {
				matched, err := regexp.MatchString(regex, sn)
				if err != nil {
					return err
				}

				if !matched {
					continue
				}

				if err := ss.selected[so].Set(attr, val); err != nil {
					return err
				}
			}
		} else {
			so, ok := StyleNames[styleName]
			if !ok {
				return errors.New("Unknown style object: " + styleName)
			}
			if err := ss.selected[so].Set(attr, val); err != nil {
				return err
			}
		}
	}

	for _, key := range defaultSection.KeyStrings() {
		tokens := strings.Split(key, ".")
		styleName, attr := tokens[0], tokens[1]
		val := defaultSection.KeysHash()[key]

		if styleName != "selected" {
			continue
		}

		for _, so := range StyleNames {
			if err := ss.selected[so].Set(attr, val); err != nil {
				return err
			}
		}
	}

	return nil
}

func (ss *StyleSet) LoadStyleSet(stylesetName string, stylesetDirs []string) error {
    filepath, err := findStyleSet(stylesetName, stylesetDirs)
    if err != nil {
        return err
    }

    file, err := ini.Load(filepath)
    if err != nil {
        return err
    }

    return ss.ParseStyleSet(file)
}

func (ss *StyleSet) LoadDefaultStyleSet() error {
    file, err := ini.Load(StyleDefaults);
    if err != nil {
        return err
    }

    return ss.ParseStyleSet(file)
}

func fnmatchToRegex(pattern string) string {
	n := len(pattern)
	var regex strings.Builder

	for i := 0; i < n; i++ {
		switch pattern[i] {
		case '*':
			regex.WriteString(".*")
		case '?':
			regex.WriteByte('.')
		default:
			regex.WriteByte(pattern[i])
		}
	}

	return regex.String()
}
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index af64ad6..fcd70ec 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -173,6 +173,20 @@ These options are configured in the *[ui]* section of aerc.conf.

	Default: 250ms

*stylesets-dirs*
	The directories where the stylesets are stored. The config takes a
	colon-seperated list of dirs.

	Default: "/usr/share/aerc/stylesets"

*styleset-name*
	The name of the styleset to be used to style the ui elements. The
	stylesets are stored in the 'stylesets' directory in the config
	directory.

	Default: default


## Contextual UI Configuration

The UI configuration can be specialized for accounts, specific mail
diff --git a/doc/aerc-stylesets.7.scd b/doc/aerc-stylesets.7.scd
new file mode 100644
index 0000000..829418e
--- /dev/null
+++ b/doc/aerc-stylesets.7.scd
@@ -0,0 +1,189 @@
aerc-stylesets(7)

# Name

aerc-stylesets - styleset file specification for *aerc*(1)

# SYNOPSIS

aerc uses a simple configuration syntax to configure the styleset for
its ui.

# Styleset Configuration

Aerc uses a simple configuration file to describe a styleset. The
styleset is described as key, value pairs. In each line, the key
represents the style object it signifies and the color/atrribute of
that is modified.

For example, in the line below, the foreground color of the
style object "msglist_unread" is set to "cornflowerblue"
```
msglist_unread.fg=cornflowerblue
```

The configuration also allows wildcard matching of the style_objects
to configure multiple style objects at a time.

## Style
The following options are available to be modified for each of the
style objects.

*fg*
	The foreground color of the style object is set.

	Syntax: `<style_object>.fg=<color>`

*bg*
	The background color of the style object is set.

	Syntax: `<style_object>.bg=<color>`

*bold*
	The bold attribute of the style object is set/unset.

	Syntax: `<style_object>.bold=<true|false|toggle>`

*blink*
	The blink attribute of the style object is set/unset.
	_The terminal needs to support blinking text_

	Syntax: `<style_object>.bold=<true|false|toggle>`

*underline*
	The underline attribute of the style object is set/unset.
	_The terminal needs to support underline text_

	Syntax: `<style_object>.underline=<true|false|toggle>`

*reverse*
	Reverses the color of the style object. Exchanges the foreground
	and background colors.

	Syntax: `<style_object>.reverse=<true|false|toggle>`
	_If the value is false, it doesn't change anything_

*normal*
	All the attributes of the style object are unset.

	Syntax: `<style_object>.normal=<true>`
	_The value doesn't matter_

*default*
	Set the style object to the default style of the context. Usually
	based on the terminal.

	Syntax: `<style_object>.default=<true>`
	_The value doesn't matter_

## Style Objects
The style objects represent the various ui elements or ui instances for
styling.

[[ *Style Object*
:[ *Description*
|  default
:  The default style object used for normal ui elements while not
   using specialized configuration.
|  error
:  The style used to show errors.
|  warning 
:  The style used when showing warnings.
|  success
:  The style used for success messages.
|  title
:  The style object used to style titles in ui elements.
|  header
:  The style object used to style headers in ui elements.
|  statusline_default
:  The default style applied to the statusline.
|  statusline_error
:  The style used for error messages in statusline.
|  statusline_success
:  The style used for success messages in statusline.
|  msglist_default
:  The default style for messages in a message list.
|  msglist_unread
:  Unread messages in a message list.
|  msglist_read
:  Read messages in a message list.
|  msglist_deleted
:  The messages marked as deleted.
|  msglist_marked
:  The messages with the marked flag.
|  msglist_flagged
:  The messages with the flagged flag.
|  dirlist_default
:  The default style for directories in the directory list.
|  completion_default
:  The default style for the completion engine.
|  completion_gutter
:  The completion gutter.
|  completion_pill
:  The completion pill.
|  tab
:  The style for the tab bar.
|  stack
:  The style for ui stack element.
|  spinner
:  The style for the loading spinner.
|  border
:  The style used to draw borders. *Only the background color is used*.
|  selecter_default
:  The default style for the selecter ui element.
|  selecter_focused
:  The focused item in a selecter ui element.
|  selecter_chooser
:  The item chooser in a selecter ui element.

## fnmatch style wildcard matching
The styleset configuration can be made simpler by using the fnmatch
style wildcard matching for the style object.

The special characters used in the fnmatch wildcards are:
[[ *Pattern*
:[ *Meaning*
|  \*
:  Matches everything
|  \?
:  Matches any single character

For example, the following wildcards can be made using this syntax.
[[ *Example*
:[ Description
|  \*.fg=blue
:  Set the foreground color of all style objects to blue.
|  \*list.bg=hotpink
:  Set the background color of all style objects that end in list 
   to hotpink.

## Selected modifier
Selected modifier can be applied to any style object. The style provided for
the selected modifier are applied on top of the style object it corresponds to.

If you would like to make sure message that are flagged as read in the msglist
appear in yellow foreground and black background. You can specify that with
this.

\tmsglist_default.selected.fg=yellow
\tmsglist_default.selected.bg=black

If we specify the global style selected modifer using fnmatch as below:

\t\*.selected.reverse=toggle

This toggles the reverse switch for selected version of all the style objects.

## Colors
The color values are set using the values accepted by the tcell library.
The values can be one of the following.

	*default*
		The color is set as per the system or terminal default.

	*<Color name>*
		Any w3c approved color name is used to set colors for the style.

	*<Hex code>*
		Hexcode for a color can be used. The format must be "\#XXXXXX"

diff --git a/lib/ui/borders.go b/lib/ui/borders.go
index 7a75759..99d6880 100644
--- a/lib/ui/borders.go
+++ b/lib/ui/borders.go
@@ -2,6 +2,8 @@ package ui

import (
	"github.com/gdamore/tcell"

	"git.sr.ht/~sircmpwn/aerc/config"
)

const (
@@ -16,12 +18,15 @@ type Bordered struct {
	borders      uint
	content      Drawable
	onInvalidate func(d Drawable)
	uiConfig     config.UIConfig
}

func NewBordered(content Drawable, borders uint) *Bordered {
func NewBordered(
	content Drawable, borders uint, uiConfig config.UIConfig) *Bordered {
	b := &Bordered{
		borders: borders,
		content: content,
		borders:  borders,
		content:  content,
		uiConfig: uiConfig,
	}
	content.OnInvalidate(b.contentInvalidated)
	return b
@@ -44,7 +49,7 @@ func (bordered *Bordered) Draw(ctx *Context) {
	y := 0
	width := ctx.Width()
	height := ctx.Height()
	style := tcell.StyleDefault.Reverse(true)
	style := bordered.uiConfig.GetStyle(config.STYLE_BORDER)
	if bordered.borders&BORDER_LEFT != 0 {
		ctx.Fill(0, 0, 1, ctx.Height(), ' ', style)
		x += 1
diff --git a/lib/ui/stack.go b/lib/ui/stack.go
index 690a869..c9004a0 100644
--- a/lib/ui/stack.go
+++ b/lib/ui/stack.go
@@ -3,16 +3,19 @@ package ui
import (
	"fmt"

	"git.sr.ht/~sircmpwn/aerc/config"

	"github.com/gdamore/tcell"
)

type Stack struct {
	children     []Drawable
	onInvalidate []func(d Drawable)
	uiConfig     config.UIConfig
}

func NewStack() *Stack {
	return &Stack{}
func NewStack(uiConfig config.UIConfig) *Stack {
	return &Stack{uiConfig: uiConfig}
}

func (stack *Stack) Children() []Drawable {
@@ -33,7 +36,8 @@ func (stack *Stack) Draw(ctx *Context) {
	if len(stack.children) > 0 {
		stack.Peek().Draw(ctx)
	} else {
		ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
		ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
			stack.uiConfig.GetStyle(config.STYLE_STACK))
	}
}

diff --git a/lib/ui/tab.go b/lib/ui/tab.go
index 4b99e4b..cd5f448 100644
--- a/lib/ui/tab.go
+++ b/lib/ui/tab.go
@@ -283,9 +283,9 @@ func (tabs *Tabs) removeHistory(index int) {
func (strip *TabStrip) Draw(ctx *Context) {
	x := 0
	for i, tab := range strip.Tabs {
		style := tcell.StyleDefault.Reverse(true)
		style := strip.uiConfig.GetStyle(config.STYLE_TAB)
		if strip.Selected == i {
			style = tcell.StyleDefault
			style = strip.uiConfig.GetStyleSelected(config.STYLE_TAB)
		}
		tabWidth := 32
		if ctx.Width()-x < tabWidth {
@@ -301,8 +301,8 @@ func (strip *TabStrip) Draw(ctx *Context) {
			break
		}
	}
	style := tcell.StyleDefault.Reverse(true)
	ctx.Fill(x, 0, ctx.Width()-x, 1, ' ', style)
	ctx.Fill(x, 0, ctx.Width()-x, 1, ' ',
		strip.uiConfig.GetStyle(config.STYLE_TAB))
}

func (strip *TabStrip) Invalidate() {
@@ -386,7 +386,8 @@ func (content *TabContent) Draw(ctx *Context) {
	if content.Selected >= len(content.Tabs) {
		width := ctx.Width()
		height := ctx.Height()
		ctx.Fill(0, 0, width, height, ' ', tcell.StyleDefault)
		ctx.Fill(0, 0, width, height, ' ',
			content.uiConfig.GetStyle(config.STYLE_TAB))
	}

	tab := content.Tabs[content.Selected]
diff --git a/lib/ui/text.go b/lib/ui/text.go
index 2b82598..455c2eb 100644
--- a/lib/ui/text.go
+++ b/lib/ui/text.go
@@ -15,17 +15,13 @@ type Text struct {
	Invalidatable
	text     string
	strategy uint
	fg       tcell.Color
	bg       tcell.Color
	bold     bool
	reverse  bool
	style    tcell.Style
}

func NewText(text string) *Text {
func NewText(text string, style tcell.Style) *Text {
	return &Text{
		bg:   tcell.ColorDefault,
		fg:   tcell.ColorDefault,
		text: text,
		text:  text,
		style: style,
	}
}

@@ -41,25 +37,6 @@ func (t *Text) Strategy(strategy uint) *Text {
	return t
}

func (t *Text) Bold(bold bool) *Text {
	t.bold = bold
	t.Invalidate()
	return t
}

func (t *Text) Color(fg tcell.Color, bg tcell.Color) *Text {
	t.fg = fg
	t.bg = bg
	t.Invalidate()
	return t
}

func (t *Text) Reverse(reverse bool) *Text {
	t.reverse = reverse
	t.Invalidate()
	return t
}

func (t *Text) Draw(ctx *Context) {
	size := runewidth.StringWidth(t.text)
	x := 0
@@ -69,15 +46,8 @@ func (t *Text) Draw(ctx *Context) {
	if t.strategy == TEXT_RIGHT {
		x = ctx.Width() - size
	}
	style := tcell.StyleDefault.Background(t.bg).Foreground(t.fg)
	if t.bold {
		style = style.Bold(true)
	}
	if t.reverse {
		style = style.Reverse(true)
	}
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
	ctx.Printf(x, 0, style, "%s", t.text)
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', t.style)
	ctx.Printf(x, 0, t.style, "%s", t.text)
}

func (t *Text) Invalidate() {
diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go
index f6b0c72..2445065 100644
--- a/lib/ui/textinput.go
+++ b/lib/ui/textinput.go
@@ -6,6 +6,8 @@ import (

	"github.com/gdamore/tcell"
	"github.com/mattn/go-runewidth"

	"git.sr.ht/~sircmpwn/aerc/config"
)

// TODO: Attach history providers
@@ -27,16 +29,18 @@ type TextInput struct {
	completeIndex     int
	completeDelay     time.Duration
	completeDebouncer *time.Timer
	uiConfig          config.UIConfig
}

// Creates a new TextInput. TextInputs will render a "textbox" in the entire
// context they're given, and process keypresses to build a string from user
// input.
func NewTextInput(text string) *TextInput {
func NewTextInput(text string, ui config.UIConfig) *TextInput {
	return &TextInput{
		cells: -1,
		text:  []rune(text),
		index: len([]rune(text)),
		cells:    -1,
		text:     []rune(text),
		index:    len([]rune(text)),
		uiConfig: ui,
	}
}

@@ -87,16 +91,18 @@ func (ti *TextInput) Draw(ctx *Context) {
		ti.ensureScroll()
	}
	ti.ctx = ctx // gross
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)

	defaultStyle := ti.uiConfig.GetStyle(config.STYLE_DEFAULT)
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle)

	text := ti.text[scroll:]
	sindex := ti.index - scroll
	if ti.password {
		x := ctx.Printf(0, 0, tcell.StyleDefault, "%s", ti.prompt)
		x := ctx.Printf(0, 0, defaultStyle, "%s", ti.prompt)
		cells := runewidth.StringWidth(string(text))
		ctx.Fill(x, 0, cells, 1, '*', tcell.StyleDefault)
		ctx.Fill(x, 0, cells, 1, '*', defaultStyle)
	} else {
		ctx.Printf(0, 0, tcell.StyleDefault, "%s%s", ti.prompt, string(text))
		ctx.Printf(0, 0, defaultStyle, "%s%s", ti.prompt, string(text))
	}
	cells := runewidth.StringWidth(string(text[:sindex]) + ti.prompt)
	if ti.focus {
@@ -126,6 +132,7 @@ func (ti *TextInput) drawPopover(ctx *Context) {
			ti.Set(stem + ti.StringRight())
			ti.Invalidate()
		},
		uiConfig: ti.uiConfig,
	}
	width := maxLen(ti.completions) + 3
	height := len(ti.completions)
@@ -353,6 +360,7 @@ type completions struct {
	onSelect   func(int)
	onExec     func()
	onStem     func(string)
	uiConfig   config.UIConfig
}

func maxLen(ss []string) int {
@@ -367,10 +375,10 @@ func maxLen(ss []string) int {
}

func (c *completions) Draw(ctx *Context) {
	bg := tcell.StyleDefault
	sel := tcell.StyleDefault.Reverse(true)
	gutter := tcell.StyleDefault
	pill := tcell.StyleDefault.Reverse(true)
	bg := c.uiConfig.GetStyle(config.STYLE_COMPLETION_DEFAULT)
	gutter := c.uiConfig.GetStyle(config.STYLE_COMPLETION_GUTTER)
	pill := c.uiConfig.GetStyle(config.STYLE_COMPLETION_PILL)
	sel := c.uiConfig.GetStyleSelected(config.STYLE_COMPLETION_DEFAULT)

	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', bg)

diff --git a/widgets/account-wizard.go b/widgets/account-wizard.go
index 4e51926..9e3db11 100644
--- a/widgets/account-wizard.go
+++ b/widgets/account-wizard.go
@@ -10,6 +10,7 @@ import (
	"path"
	"strconv"
	"strings"
	"time"

	"github.com/gdamore/tcell"
	"github.com/go-ini/ini"
@@ -75,21 +76,21 @@ type AccountWizard struct {

func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
	wizard := &AccountWizard{
		accountName:  ui.NewTextInput("").Prompt("> "),
		accountName:  ui.NewTextInput("", conf.Ui).Prompt("> "),
		aerc:         aerc,
		conf:         conf,
		temporary:    false,
		copySent:     true,
		email:        ui.NewTextInput("").Prompt("> "),
		fullName:     ui.NewTextInput("").Prompt("> "),
		imapPassword: ui.NewTextInput("").Prompt("] ").Password(true),
		imapServer:   ui.NewTextInput("").Prompt("> "),
		imapStr:      ui.NewText("imaps://"),
		imapUsername: ui.NewTextInput("").Prompt("> "),
		smtpPassword: ui.NewTextInput("").Prompt("] ").Password(true),
		smtpServer:   ui.NewTextInput("").Prompt("> "),
		smtpStr:      ui.NewText("smtps://"),
		smtpUsername: ui.NewTextInput("").Prompt("> "),
		email:        ui.NewTextInput("", conf.Ui).Prompt("> "),
		fullName:     ui.NewTextInput("", conf.Ui).Prompt("> "),
		imapPassword: ui.NewTextInput("", conf.Ui).Prompt("] ").Password(true),
		imapServer:   ui.NewTextInput("", conf.Ui).Prompt("> "),
		imapStr:      ui.NewText("imaps://", conf.Ui.GetStyle(config.STYLE_DEFAULT)),
		imapUsername: ui.NewTextInput("", conf.Ui).Prompt("> "),
		smtpPassword: ui.NewTextInput("", conf.Ui).Prompt("] ").Password(true),
		smtpServer:   ui.NewTextInput("", conf.Ui).Prompt("> "),
		smtpStr:      ui.NewText("smtps://", conf.Ui.GetStyle(config.STYLE_DEFAULT)),
		smtpUsername: ui.NewTextInput("", conf.Ui).Prompt("> "),
	}

	// Autofill some stuff for the user
@@ -150,33 +151,36 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
		{ui.SIZE_WEIGHT, ui.Const(1)},
	})
	basics.AddChild(
		ui.NewText("\nWelcome to aerc! Let's configure your account.\n\n" +
			"This wizard supports basic IMAP & SMTP configuration.\n" +
			"For other configurations, use <Ctrl+q> to exit and read the " +
			"aerc-config(5) man page.\n" +
			"Press <Tab> and <Shift+Tab> to cycle between each field in this form, or <Ctrl+j> and <Ctrl+k>."))
		ui.NewText("\nWelcome to aerc! Let's configure your account.\n\n"+
			"This wizard supports basic IMAP & SMTP configuration.\n"+
			"For other configurations, use <Ctrl+q> to exit and read the "+
			"aerc-config(5) man page.\n"+
			"Press <Tab> and <Shift+Tab> to cycle between each field in this form, "+
			"or <Ctrl+j> and <Ctrl+k>.",
			conf.Ui.GetStyle(config.STYLE_DEFAULT)))
	basics.AddChild(
		ui.NewText("Name for this account? (e.g. 'Personal' or 'Work')").
			Bold(true)).
		ui.NewText("Name for this account? (e.g. 'Personal' or 'Work')",
			conf.Ui.GetStyle(config.STYLE_HEADER))).
		At(1, 0)
	basics.AddChild(wizard.accountName).
		At(2, 0)
	basics.AddChild(ui.NewFill(' ')).
		At(3, 0)
	basics.AddChild(
		ui.NewText("Full name for outgoing emails? (e.g. 'John Doe')").
			Bold(true)).
		ui.NewText("Full name for outgoing emails? (e.g. 'John Doe')",
			conf.Ui.GetStyle(config.STYLE_HEADER))).
		At(4, 0)
	basics.AddChild(wizard.fullName).
		At(5, 0)
	basics.AddChild(ui.NewFill(' ')).
		At(6, 0)
	basics.AddChild(
		ui.NewText("Your email address? (e.g. 'john@example.org')").Bold(true)).
		ui.NewText("Your email address? (e.g. 'john@example.org')",
			conf.Ui.GetStyle(config.STYLE_HEADER))).
		At(7, 0)
	basics.AddChild(wizard.email).
		At(8, 0)
	selecter := NewSelecter([]string{"Next"}, 0).
	selecter := NewSelecter([]string{"Next"}, 0, conf.Ui).
		OnChoose(func(option string) {
			email := wizard.email.String()
			if strings.ContainsRune(email, '@') {
@@ -227,16 +231,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
	}).Columns([]ui.GridSpec{
		{ui.SIZE_WEIGHT, ui.Const(1)},
	})
	incoming.AddChild(ui.NewText("\nConfigure incoming mail (IMAP)"))
	incoming.AddChild(ui.NewText("\nConfigure incoming mail (IMAP)",
		conf.Ui.GetStyle(config.STYLE_DEFAULT)))
	incoming.AddChild(
		ui.NewText("Username").Bold(true)).
		ui.NewText("Username",
			conf.Ui.GetStyle(config.STYLE_HEADER))).
		At(1, 0)
	incoming.AddChild(wizard.imapUsername).
		At(2, 0)
	incoming.AddChild(ui.NewFill(' ')).
		At(3, 0)
	incoming.AddChild(
		ui.NewText("Password").Bold(true)).
		ui.NewText("Password",
			conf.Ui.GetStyle(config.STYLE_HEADER))).
		At(4, 0)
	incoming.AddChild(wizard.imapPassword).
		At(5, 0)
@@ -244,20 +251,22 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
		At(6, 0)
	incoming.AddChild(
		ui.NewText("Server address "+
			"(e.g. 'mail.example.org' or 'mail.example.org:1313')").Bold(true)).
			"(e.g. 'mail.example.org' or 'mail.example.org:1313')",
			conf.Ui.GetStyle(config.STYLE_HEADER))).
		At(7, 0)
	incoming.AddChild(wizard.imapServer).
		At(8, 0)
	incoming.AddChild(ui.NewFill(' ')).
		At(9, 0)
	incoming.AddChild(
		ui.NewText("Connection mode").Bold(true)).
		ui.NewText("Connection mode",
			conf.Ui.GetStyle(config.STYLE_HEADER))).
		At(10, 0)
	imapMode := NewSelecter([]string{
		"IMAP over SSL/TLS",
		"IMAP with STARTTLS",
		"Insecure IMAP",
	}, 0).Chooser(true).OnSelect(func(option string) {
	}, 0, conf.Ui).Chooser(true).OnSelect(func(option string) {
		switch option {
		case "IMAP over SSL/TLS":
			wizard.imapMode = IMAP_OVER_TLS
@@ -269,7 +278,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
		wizard.imapUri()
	})
	incoming.AddChild(imapMode).At(11, 0)
	selecter = NewSelecter([]string{"Previous", "Next"}, 1).
	selecter = NewSelecter([]string{"Previous", "Next"}, 1, conf.Ui).
		OnChoose(wizard.advance)
	incoming.AddChild(ui.NewFill(' ')).At(12, 0)
	incoming.AddChild(wizard.imapStr).At(13, 0)
@@ -304,16 +313,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
	}).Columns([]ui.GridSpec{
		{ui.SIZE_WEIGHT, ui.Const(1)},
	})
	outgoing.AddChild(ui.NewText("\nConfigure outgoing mail (SMTP)"))
	outgoing.AddChild(ui.NewText("\nConfigure outgoing mail (SMTP)",
		conf.Ui.GetStyle(config.STYLE_DEFAULT)))
	outgoing.AddChild(
		ui.NewText("Username").Bold(true)).
		ui.NewText("Username",
			conf.Ui.GetStyle(config.STYLE_HEADER))).
		At(1, 0)
	outgoing.AddChild(wizard.smtpUsername).
		At(2, 0)
	outgoing.AddChild(ui.NewFill(' ')).
		At(3, 0)
	outgoing.AddChild(
		ui.NewText("Password").Bold(true)).
		ui.NewText("Password",
			conf.Ui.GetStyle(config.STYLE_HEADER))).
		At(4, 0)
	outgoing.AddChild(wizard.smtpPassword).
		At(5, 0)
@@ -321,20 +333,22 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
		At(6, 0)
	outgoing.AddChild(
		ui.NewText("Server address "+
			"(e.g. 'mail.example.org' or 'mail.example.org:1313')").Bold(true)).
			"(e.g. 'mail.example.org' or 'mail.example.org:1313')",
			conf.Ui.GetStyle(config.STYLE_HEADER))).
		At(7, 0)
	outgoing.AddChild(wizard.smtpServer).
		At(8, 0)
	outgoing.AddChild(ui.NewFill(' ')).
		At(9, 0)
	outgoing.AddChild(
		ui.NewText("Connection mode").Bold(true)).
		ui.NewText("Connection mode",
			conf.Ui.GetStyle(config.STYLE_HEADER))).
		At(10, 0)
	smtpMode := NewSelecter([]string{
		"SMTP over SSL/TLS",
		"SMTP with STARTTLS",
		"Insecure SMTP",
	}, 0).Chooser(true).OnSelect(func(option string) {
	}, 0, conf.Ui).Chooser(true).OnSelect(func(option string) {
		switch option {
		case "SMTP over SSL/TLS":
			wizard.smtpMode = SMTP_OVER_TLS
@@ -346,15 +360,15 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
		wizard.smtpUri()
	})
	outgoing.AddChild(smtpMode).At(11, 0)
	selecter = NewSelecter([]string{"Previous", "Next"}, 1).
	selecter = NewSelecter([]string{"Previous", "Next"}, 1, conf.Ui).
		OnChoose(wizard.advance)
	outgoing.AddChild(ui.NewFill(' ')).At(12, 0)
	outgoing.AddChild(wizard.smtpStr).At(13, 0)
	outgoing.AddChild(ui.NewFill(' ')).At(14, 0)
	outgoing.AddChild(
		ui.NewText("Copy sent messages to 'Sent' folder?").Bold(true)).
		At(15, 0)
	copySent := NewSelecter([]string{"Yes", "No"}, 0).
		ui.NewText("Copy sent messages to 'Sent' folder?",
			conf.Ui.GetStyle(config.STYLE_HEADER))).At(15, 0)
	copySent := NewSelecter([]string{"Yes", "No"}, 0, conf.Ui).
		Chooser(true).OnChoose(func(option string) {
		switch option {
		case "Yes":
@@ -380,15 +394,16 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
		{ui.SIZE_WEIGHT, ui.Const(1)},
	})
	complete.AddChild(ui.NewText(
		"\nConfiguration complete!\n\n" +
			"You can go back and double check your settings, or choose 'Finish' to\n" +
			"save your settings to accounts.conf.\n\n" +
			"To add another account in the future, run ':new-account'."))
		"\nConfiguration complete!\n\n"+
			"You can go back and double check your settings, or choose 'Finish' to\n"+
			"save your settings to accounts.conf.\n\n"+
			"To add another account in the future, run ':new-account'.",
		conf.Ui.GetStyle(config.STYLE_DEFAULT)))
	selecter = NewSelecter([]string{
		"Previous",
		"Finish & open tutorial",
		"Finish",
	}, 1).OnChoose(func(option string) {
	}, 1, conf.Ui).OnChoose(func(option string) {
		switch option {
		case "Previous":
			wizard.advance("Previous")
@@ -414,7 +429,7 @@ func (wizard *AccountWizard) ConfigureTemporaryAccount(temporary bool) {

func (wizard *AccountWizard) errorFor(d ui.Interactive, err error) {
	if d == nil {
		wizard.aerc.PushError(" " + err.Error())
		wizard.aerc.PushError(" "+err.Error(), 10*time.Second)
		wizard.Invalidate()
		return
	}
@@ -429,7 +444,7 @@ func (wizard *AccountWizard) errorFor(d ui.Interactive, err error) {
				wizard.step = step
				wizard.focus = focus
				wizard.Focus(true)
				wizard.aerc.PushError(" " + err.Error())
				wizard.aerc.PushError(" "+err.Error(), 10*time.Second)
				wizard.Invalidate()
				return
			}
@@ -540,7 +555,7 @@ func (wizard *AccountWizard) finish(tutorial bool) {
		term.OnClose = func(err error) {
			wizard.aerc.RemoveTab(term)
			if err != nil {
				wizard.aerc.PushError(" " + err.Error())
				wizard.aerc.PushError(" "+err.Error(), 10*time.Second)
			}
		}
	}
diff --git a/widgets/account.go b/widgets/account.go
index 211f09d..6567638 100644
--- a/widgets/account.go
+++ b/widgets/account.go
@@ -4,6 +4,7 @@ import (
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/gdamore/tcell"

@@ -64,15 +65,14 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon

	worker, err := worker.NewWorker(acct.Source, logger)
	if err != nil {
		host.SetStatus(fmt.Sprintf("%s: %s", acct.Name, err)).
			Color(tcell.ColorDefault, tcell.ColorRed)
		host.SetError(fmt.Sprintf("%s: %s", acct.Name, err))
		return view
	}
	view.worker = worker

	view.dirlist = NewDirectoryList(conf, acct, logger, worker)
	if acctUiConf.SidebarWidth > 0 {
		view.grid.AddChild(ui.NewBordered(view.dirlist, ui.BORDER_RIGHT))
		view.grid.AddChild(ui.NewBordered(view.dirlist, ui.BORDER_RIGHT, acctUiConf))
	}

	view.msglist = NewMessageList(conf, logger, aerc)
@@ -273,7 +273,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
		acct.labels = msg.Labels
	case *types.Error:
		acct.logger.Printf("%v", msg.Error)
		acct.aerc.PushError(fmt.Sprintf("%v", msg.Error))
		acct.aerc.PushError(fmt.Sprintf("%v", msg.Error), 10*time.Second)
	}
}

@@ -283,7 +283,7 @@ func (acct *AccountView) getSortCriteria() []*types.SortCriterion {
	}
	criteria, err := sort.GetSortCriteria(acct.UiConfig().Sort)
	if err != nil {
		acct.aerc.PushError(" ui.sort: " + err.Error())
		acct.aerc.PushError(" ui.sort: "+err.Error(), 10*time.Second)
		return nil
	}
	return criteria
diff --git a/widgets/aerc.go b/widgets/aerc.go
index 4913be3..367335d 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -51,8 +51,8 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,

	tabs := ui.NewTabs(&conf.Ui)

	statusbar := ui.NewStack()
	statusline := NewStatusLine()
	statusbar := ui.NewStack(conf.Ui)
	statusline := NewStatusLine(conf.Ui)
	statusbar.Push(statusline)

	grid := ui.NewGrid().Rows([]ui.GridSpec{
@@ -76,7 +76,7 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
		logger:     logger,
		statusbar:  statusbar,
		statusline: statusline,
		prompts:    ui.NewStack(),
		prompts:    ui.NewStack(conf.Ui),
		tabs:       tabs,
	}

@@ -382,12 +382,20 @@ func (aerc *Aerc) SetStatus(status string) *StatusMessage {
	return aerc.statusline.Set(status)
}

func (aerc *Aerc) SetError(status string) *StatusMessage {
	return aerc.statusline.SetError(status)
}

func (aerc *Aerc) PushStatus(text string, expiry time.Duration) *StatusMessage {
	return aerc.statusline.Push(text, expiry)
}

func (aerc *Aerc) PushError(text string) {
	aerc.PushStatus(text, 10*time.Second).Color(tcell.ColorDefault, tcell.ColorRed)
func (aerc *Aerc) PushError(text string, expiry time.Duration) *StatusMessage {
	return aerc.statusline.PushError(text, expiry)
}

func (aerc *Aerc) PushSuccess(text string, expiry time.Duration) *StatusMessage {
	return aerc.statusline.PushSuccess(text, expiry)
}

func (aerc *Aerc) focus(item ui.Interactive) {
@@ -416,11 +424,11 @@ func (aerc *Aerc) BeginExCommand(cmd string) {
	exline := NewExLine(aerc.conf, cmd, func(cmd string) {
		parts, err := shlex.Split(cmd)
		if err != nil {
			aerc.PushError(" " + err.Error())
			aerc.PushError(" "+err.Error(), 10*time.Second)
		}
		err = aerc.cmd(parts)
		if err != nil {
			aerc.PushError(" " + err.Error())
			aerc.PushError(" "+err.Error(), 10*time.Second)
		}
		// only add to history if this is an unsimulated command,
		// ie one not executed from a keybinding
@@ -444,7 +452,7 @@ func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) {
		}
		err := aerc.cmd(cmd)
		if err != nil {
			aerc.PushError(" " + err.Error())
			aerc.PushError(" "+err.Error(), 10*time.Second)
		}
	}, func(cmd string) []string {
		return nil // TODO: completions
@@ -471,7 +479,7 @@ func (aerc *Aerc) RegisterChoices(choices []Choice) {
		}
		err := aerc.cmd(cmd)
		if err != nil {
			aerc.PushError(" " + err.Error())
			aerc.PushError(" "+err.Error(), 10*time.Second)
		}
	}, func(cmd string) []string {
		return nil // TODO: completions
@@ -555,7 +563,7 @@ func (aerc *Aerc) CloseDialog() {
func (aerc *Aerc) GetPassword(title string, prompt string) (chText chan string, chErr chan error) {
	chText = make(chan string, 1)
	chErr = make(chan error, 1)
	getPasswd := NewGetPasswd(title, prompt, func(pw string, err error) {
	getPasswd := NewGetPasswd(title, prompt, aerc.conf, func(pw string, err error) {
		defer func() {
			close(chErr)
			close(chText)
diff --git a/widgets/compose.go b/widgets/compose.go
index b68c406..39e40cd 100644
--- a/widgets/compose.go
+++ b/widgets/compose.go
@@ -72,10 +72,11 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,

	templateData := templates.ParseTemplateData(defaults, original)
	cmpl := completer.New(conf.Compose.AddressBookCmd, func(err error) {
		aerc.PushError(fmt.Sprintf("could not complete header: %v", err))
		aerc.PushError(
			fmt.Sprintf("could not complete header: %v", err), 10*time.Second)
		worker.Logger.Printf("could not complete header: %v", err)
	}, aerc.Logger())
	layout, editors, focusable := buildComposeHeader(conf, cmpl, defaults)
	layout, editors, focusable := buildComposeHeader(aerc, cmpl, defaults)

	email, err := ioutil.TempFile("", "aerc-compose-*.eml")
	if err != nil {
@@ -112,21 +113,21 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
	return c, nil
}

func buildComposeHeader(conf *config.AercConfig, cmpl *completer.Completer,
func buildComposeHeader(aerc *Aerc, cmpl *completer.Completer,
	defaults map[string]string) (
	newLayout HeaderLayout,
	editors map[string]*headerEditor,
	focusable []ui.MouseableDrawableInteractive,
) {
	layout := conf.Compose.HeaderLayout
	layout := aerc.conf.Compose.HeaderLayout
	editors = make(map[string]*headerEditor)
	focusable = make([]ui.MouseableDrawableInteractive, 0)

	for _, row := range layout {
		for _, h := range row {
			e := newHeaderEditor(h, "")
			if conf.Ui.CompletionPopovers {
				e.input.TabComplete(cmpl.ForHeader(h), conf.Ui.CompletionDelay)
			e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig())
			if aerc.conf.Ui.CompletionPopovers {
				e.input.TabComplete(cmpl.ForHeader(h), aerc.SelectedAccount().UiConfig().CompletionDelay)
			}
			editors[h] = e
			switch h {
@@ -143,9 +144,9 @@ func buildComposeHeader(conf *config.AercConfig, cmpl *completer.Completer,
	for _, h := range []string{"Cc", "Bcc"} {
		if val, ok := defaults[h]; ok && val != "" {
			if _, ok := editors[h]; !ok {
				e := newHeaderEditor(h, "")
				if conf.Ui.CompletionPopovers {
					e.input.TabComplete(cmpl.ForHeader(h), conf.Ui.CompletionDelay)
				e := newHeaderEditor(h, "", aerc.SelectedAccount().UiConfig())
				if aerc.conf.Ui.CompletionPopovers {
					e.input.TabComplete(cmpl.ForHeader(h), aerc.SelectedAccount().UiConfig().CompletionDelay)
				}
				editors[h] = e
				focusable = append(focusable, e)
@@ -259,7 +260,9 @@ func (c *Composer) readSignatureFromFile() []byte {
	}
	signature, err := ioutil.ReadFile(sigFile)
	if err != nil {
		c.aerc.PushError(fmt.Sprintf(" Error loading signature from file: %v", sigFile))
		c.aerc.PushError(
			fmt.Sprintf(" Error loading signature from file: %v", sigFile),
			10*time.Second)
		return nil
	}
	return signature
@@ -648,7 +651,7 @@ func (c *Composer) AddEditor(header string, value string, appendHeader bool) {
		}
		return
	}
	e := newHeaderEditor(header, value)
	e := newHeaderEditor(header, value, c.aerc.SelectedAccount().UiConfig())
	if c.config.Ui.CompletionPopovers {
		e.input.TabComplete(c.completer.ForHeader(header), c.config.Ui.CompletionDelay)
	}
@@ -704,23 +707,27 @@ func (c *Composer) reloadEmail() error {
}

type headerEditor struct {
	name    string
	focused bool
	input   *ui.TextInput
	name     string
	focused  bool
	input    *ui.TextInput
	uiConfig config.UIConfig
}

func newHeaderEditor(name string, value string) *headerEditor {
func newHeaderEditor(name string, value string, uiConfig config.UIConfig) *headerEditor {
	return &headerEditor{
		input: ui.NewTextInput(value),
		name:  name,
		input:    ui.NewTextInput(value, uiConfig),
		name:     name,
		uiConfig: uiConfig,
	}
}

func (he *headerEditor) Draw(ctx *ui.Context) {
	name := he.name + " "
	size := runewidth.StringWidth(name)
	ctx.Fill(0, 0, size, ctx.Height(), ' ', tcell.StyleDefault)
	ctx.Printf(0, 0, tcell.StyleDefault.Bold(true), "%s", name)
	defaultStyle := he.uiConfig.GetStyle(config.STYLE_DEFAULT)
	headerStyle := he.uiConfig.GetStyle(config.STYLE_HEADER)
	ctx.Fill(0, 0, size, ctx.Height(), ' ', defaultStyle)
	ctx.Printf(0, 0, headerStyle, "%s", name)
	he.input.Draw(ctx.Subcontext(size, 0, ctx.Width()-size, 1))
}

@@ -784,21 +791,25 @@ func newReviewMessage(composer *Composer, err error) *reviewMessage {
		{ui.SIZE_WEIGHT, ui.Const(1)},
	})

	uiConfig := composer.config.Ui

	if err != nil {
		grid.AddChild(ui.NewText(err.Error()).
			Color(tcell.ColorRed, tcell.ColorDefault))
		grid.AddChild(ui.NewText("Press [q] to close this tab.")).At(1, 0)
		grid.AddChild(ui.NewText(err.Error(), uiConfig.GetStyle(config.STYLE_ERROR)))
		grid.AddChild(ui.NewText("Press [q] to close this tab.",
			uiConfig.GetStyle(config.STYLE_DEFAULT))).At(1, 0)
	} else {
		// TODO: source this from actual keybindings?
		grid.AddChild(ui.NewText(
			"Send this email? [y]es/[n]o/[p]ostpone/[e]dit/[a]ttach")).At(0, 0)
		grid.AddChild(ui.NewText("Attachments:").
			Reverse(true)).At(1, 0)
		grid.AddChild(ui.NewText("Send this email? [y]es/[n]o/[e]dit/[a]ttach",
			uiConfig.GetStyle(config.STYLE_DEFAULT))).At(0, 0)
		grid.AddChild(ui.NewText("Attachments:",
			uiConfig.GetStyle(config.STYLE_TITLE))).At(1, 0)
		if len(composer.attachments) == 0 {
			grid.AddChild(ui.NewText("(none)")).At(2, 0)
			grid.AddChild(ui.NewText("(none)",
				uiConfig.GetStyle(config.STYLE_DEFAULT))).At(2, 0)
		} else {
			for i, a := range composer.attachments {
				grid.AddChild(ui.NewText(a)).At(i+2, 0)
				grid.AddChild(ui.NewText(a, uiConfig.GetStyle(config.STYLE_DEFAULT))).
					At(i+2, 0)
			}
		}
	}
diff --git a/widgets/dirlist.go b/widgets/dirlist.go
index 3711544..3ed79cc 100644
--- a/widgets/dirlist.go
+++ b/widgets/dirlist.go
@@ -196,7 +196,8 @@ func (dirlist *DirectoryList) getRUEString(name string) string {
}

func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
		dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT))

	if dirlist.spinner.IsRunning() {
		dirlist.spinner.Draw(ctx)
@@ -204,7 +205,7 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
	}

	if len(dirlist.dirs) == 0 {
		style := tcell.StyleDefault
		style := dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT)
		ctx.Printf(0, 0, style, dirlist.UiConfig().EmptyDirlist)
		return
	}
@@ -236,10 +237,7 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {

		style := tcell.StyleDefault
		if name == dirlist.selected {
			style = style.Reverse(true)
		} else if name == dirlist.selecting {
			style = style.Reverse(true)
			style = style.Foreground(tcell.ColorGray)
			style = dirlist.UiConfig().GetStyleSelected(config.STYLE_DIRLIST_DEFAULT)
		}
		ctx.Fill(0, row, textWidth, 1, ' ', style)

diff --git a/widgets/exline.go b/widgets/exline.go
index 6def938..692c8e2 100644
--- a/widgets/exline.go
+++ b/widgets/exline.go
@@ -15,13 +15,14 @@ type ExLine struct {
	tabcomplete func(cmd string) []string
	cmdHistory  lib.History
	input       *ui.TextInput
	conf        *config.AercConfig
}

func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), finish func(),
	tabcomplete func(cmd string) []string,
	cmdHistory lib.History) *ExLine {

	input := ui.NewTextInput("").Prompt(":").Set(cmd)
	input := ui.NewTextInput("", conf.Ui).Prompt(":").Set(cmd)
	if conf.Ui.CompletionPopovers {
		input.TabComplete(tabcomplete, conf.Ui.CompletionDelay)
	}
@@ -31,6 +32,7 @@ func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), fin
		tabcomplete: tabcomplete,
		cmdHistory:  cmdHistory,
		input:       input,
		conf:        conf,
	}
	input.OnInvalidate(func(d ui.Drawable) {
		exline.Invalidate()
@@ -41,7 +43,7 @@ func NewExLine(conf *config.AercConfig, cmd string, commit func(cmd string), fin
func NewPrompt(conf *config.AercConfig, prompt string, commit func(text string),
	tabcomplete func(cmd string) []string) *ExLine {

	input := ui.NewTextInput("").Prompt(prompt)
	input := ui.NewTextInput("", conf.Ui).Prompt(prompt)
	if conf.Ui.CompletionPopovers {
		input.TabComplete(tabcomplete, conf.Ui.CompletionDelay)
	}
diff --git a/widgets/getpasswd.go b/widgets/getpasswd.go
index 34f8b1f..b3ea9e0 100644
--- a/widgets/getpasswd.go
+++ b/widgets/getpasswd.go
@@ -5,6 +5,7 @@ import (

	"github.com/gdamore/tcell"

	"git.sr.ht/~sircmpwn/aerc/config"
	"git.sr.ht/~sircmpwn/aerc/lib/ui"
)

@@ -14,14 +15,16 @@ type GetPasswd struct {
	title    string
	prompt   string
	input    *ui.TextInput
	conf     *config.AercConfig
}

func NewGetPasswd(title string, prompt string, cb func(string, error)) *GetPasswd {
func NewGetPasswd(title string, prompt string, conf *config.AercConfig, cb func(string, error)) *GetPasswd {
	getpasswd := &GetPasswd{
		callback: cb,
		title:    title,
		prompt:   prompt,
		input:    ui.NewTextInput("").Password(true).Prompt("Password: "),
		conf:     conf,
		input:    ui.NewTextInput("", conf.Ui).Password(true).Prompt("Password: "),
	}
	getpasswd.input.OnInvalidate(func(_ ui.Drawable) {
		getpasswd.Invalidate()
@@ -31,10 +34,13 @@ func NewGetPasswd(title string, prompt string, cb func(string, error)) *GetPassw
}

func (gp *GetPasswd) Draw(ctx *ui.Context) {
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
	ctx.Fill(0, 0, ctx.Width(), 1, ' ', tcell.StyleDefault.Reverse(true))
	ctx.Printf(1, 0, tcell.StyleDefault.Reverse(true), "%s", gp.title)
	ctx.Printf(1, 1, tcell.StyleDefault, gp.prompt)
	defaultStyle := gp.conf.Ui.GetStyle(config.STYLE_DEFAULT)
	titleStyle := gp.conf.Ui.GetStyle(config.STYLE_TITLE)

	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle)
	ctx.Fill(0, 0, ctx.Width(), 1, ' ', titleStyle)
	ctx.Printf(1, 0, titleStyle, "%s", gp.title)
	ctx.Printf(1, 1, defaultStyle, gp.prompt)
	gp.input.Draw(ctx.Subcontext(1, 3, ctx.Width()-2, 1))
}

diff --git a/widgets/msglist.go b/widgets/msglist.go
index 1ed6bb1..a98e79c 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -4,6 +4,7 @@ import (
	"fmt"
	"log"
	"math"
	"time"

	"github.com/gdamore/tcell"
	"github.com/mattn/go-runewidth"
@@ -50,7 +51,8 @@ func (ml *MessageList) Invalidate() {

func (ml *MessageList) Draw(ctx *ui.Context) {
	ml.height = ctx.Height()
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
		ml.aerc.SelectedAccount().UiConfig().GetStyle(config.STYLE_MSGLIST_DEFAULT))

	store := ml.Store()
	if store == nil {
@@ -101,38 +103,50 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
			continue
		}

		style := tcell.StyleDefault
		uiConfig := ml.conf.GetUiConfig(map[config.ContextType]string{
			config.UI_CONTEXT_ACCOUNT: ml.aerc.SelectedAccount().AccountConfig().Name,
			config.UI_CONTEXT_FOLDER:  ml.aerc.SelectedAccount().Directories().Selected(),
			config.UI_CONTEXT_SUBJECT: msg.Envelope.Subject,
		})

		so := config.STYLE_MSGLIST_DEFAULT

		// current row
		if row == ml.store.SelectedIndex()-ml.scroll {
			style = style.Reverse(true)
		}
		// deleted message
		if _, ok := store.Deleted[msg.Uid]; ok {
			style = style.Foreground(tcell.ColorGray)
			so = config.STYLE_MSGLIST_DELETED
		}
		// unread message
		seen := false
		flagged := false
		for _, flag := range msg.Flags {
			if flag == models.SeenFlag {
			switch flag {
			case models.SeenFlag:
				seen = true
			case models.FlaggedFlag:
				flagged = true
			}
		}
		if !seen {
			style = style.Bold(true)
			so = config.STYLE_MSGLIST_UNREAD
		}

		ctx.Fill(0, row, textWidth, 1, ' ', style)
		if flagged {
			so = config.STYLE_MSGLIST_FLAGGED
		}

		confParams := map[config.ContextType]string{
			config.UI_CONTEXT_ACCOUNT: ml.aerc.SelectedAccount().AccountConfig().Name,
			config.UI_CONTEXT_FOLDER:  ml.aerc.SelectedAccount().Directories().Selected(),
		// marked message
		if store.IsMarked(msg.Uid) {
			so = config.STYLE_MSGLIST_MARKED
		}
		if msg.Envelope != nil {
			confParams[config.UI_CONTEXT_SUBJECT] = msg.Envelope.Subject

		style := uiConfig.GetStyle(so)

		// current row
		if row == ml.store.SelectedIndex()-ml.scroll {
			style = uiConfig.GetStyleSelected(so)
		}
		uiConfig := ml.conf.GetUiConfig(confParams)

		ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
		fmtStr, args, err := format.ParseMessageFormat(
			ml.aerc.SelectedAccount().acct.From,
			uiConfig.IndexFormat,
@@ -208,7 +222,7 @@ func (ml *MessageList) MouseEvent(localX int, localY int, event tcell.Event) {
				lib.NewMessageStoreView(msg, store, ml.aerc.DecryptKeys,
					func(view lib.MessageView, err error) {
						if err != nil {
							ml.aerc.PushError(err.Error())
							ml.aerc.PushError(err.Error(), 10*time.Second)
							return
						}
						viewer := NewMessageViewer(acct, ml.aerc.Config(), view)
@@ -342,7 +356,8 @@ func (ml *MessageList) ensureScroll() {
}

func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) {
	msg := ml.aerc.SelectedAccount().UiConfig().EmptyMessage
	uiConfig := ml.aerc.SelectedAccount().UiConfig()
	msg := uiConfig.EmptyMessage
	ctx.Printf((ctx.Width()/2)-(len(msg)/2), 0,
		tcell.StyleDefault, "%s", msg)
		uiConfig.GetStyle(config.STYLE_MSGLIST_DEFAULT), "%s", msg)
}
diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go
index f06b787..29e07b2 100644
--- a/widgets/msgviewer.go
+++ b/widgets/msgviewer.go
@@ -32,6 +32,7 @@ type MessageViewer struct {
	grid     *ui.Grid
	switcher *PartSwitcher
	msg      lib.MessageView
	uiConfig config.UIConfig
}

type PartSwitcher struct {
@@ -61,9 +62,11 @@ func NewMessageViewer(acct *AccountView,
	header, headerHeight := layout.grid(
		func(header string) ui.Drawable {
			return &HeaderView{
				conf: conf,
				Name: header,
				Value: fmtHeader(msg.MessageInfo(), header,
					acct.UiConfig().TimestampFormat),
				uiConfig: acct.UiConfig(),
			}
		},
	)
@@ -93,15 +96,16 @@ func NewMessageViewer(acct *AccountView,
	err := createSwitcher(acct, switcher, conf, msg)
	if err != nil {
		return &MessageViewer{
			err:  err,
			grid: grid,
			msg:  msg,
			err:      err,
			grid:     grid,
			msg:      msg,
			uiConfig: acct.UiConfig(),
		}
	}

	grid.AddChild(header).At(0, 0)
	if msg.PGPDetails() != nil {
		grid.AddChild(NewPGPInfo(msg.PGPDetails())).At(1, 0)
		grid.AddChild(NewPGPInfo(msg.PGPDetails(), acct.UiConfig())).At(1, 0)
		grid.AddChild(ui.NewFill(' ')).At(2, 0)
		grid.AddChild(switcher).At(3, 0)
	} else {
@@ -115,6 +119,7 @@ func NewMessageViewer(acct *AccountView,
		grid:     grid,
		msg:      msg,
		switcher: switcher,
		uiConfig: acct.UiConfig(),
	}
	switcher.mv = mv

@@ -223,8 +228,9 @@ func createSwitcher(acct *AccountView, switcher *PartSwitcher,

func (mv *MessageViewer) Draw(ctx *ui.Context) {
	if mv.err != nil {
		ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
		ctx.Printf(0, 0, tcell.StyleDefault, "%s", mv.err.Error())
		style := mv.acct.UiConfig().GetStyle(config.STYLE_DEFAULT)
		ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
		ctx.Printf(0, 0, style, "%s", mv.err.Error())
		return
	}
	mv.grid.Draw(ctx)
@@ -346,7 +352,10 @@ func (ps *PartSwitcher) Draw(ctx *ui.Context) {
	ps.height = ctx.Height()
	y := ctx.Height() - height
	for i, part := range ps.parts {
		style := tcell.StyleDefault.Reverse(ps.selected == i)
		style := ps.mv.uiConfig.GetStyle(config.STYLE_DEFAULT)
		if ps.selected == i {
			style = ps.mv.uiConfig.GetStyleSelected(config.STYLE_DEFAULT)
		}
		ctx.Fill(0, y+i, ctx.Width(), 1, ' ', style)
		name := fmt.Sprintf("%s/%s",
			strings.ToLower(part.part.MIMEType),
@@ -435,6 +444,7 @@ func (mv *MessageViewer) Focus(focus bool) {

type PartViewer struct {
	ui.Invalidatable
	conf        *config.AercConfig
	err         error
	fetched     bool
	filter      *exec.Cmd
@@ -449,6 +459,7 @@ type PartViewer struct {
	term        *Terminal
	selecter    *Selecter
	grid        *ui.Grid
	uiConfig    config.UIConfig
}

func NewPartViewer(acct *AccountView, conf *config.AercConfig,
@@ -518,7 +529,8 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
		{ui.SIZE_WEIGHT, ui.Const(1)},
	})

	selecter := NewSelecter([]string{"Save message", "Pipe to command"}, 0).
	selecter := NewSelecter([]string{"Save message", "Pipe to command"},
		0, acct.UiConfig()).
		OnChoose(func(option string) {
			switch option {
			case "Save message":
@@ -531,6 +543,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
	grid.AddChild(selecter).At(2, 0)

	pv := &PartViewer{
		conf:        conf,
		filter:      filter,
		index:       index,
		msg:         msg,
@@ -542,6 +555,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
		term:        term,
		selecter:    selecter,
		grid:        grid,
		uiConfig:    acct.UiConfig(),
	}

	if term != nil {
@@ -639,14 +653,16 @@ func (pv *PartViewer) Invalidate() {
}

func (pv *PartViewer) Draw(ctx *ui.Context) {
	style := pv.uiConfig.GetStyle(config.STYLE_DEFAULT)
	styleError := pv.uiConfig.GetStyle(config.STYLE_ERROR)
	if pv.filter == nil {
		// TODO: Let them download it directly or something
		ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
		ctx.Printf(0, 0, tcell.StyleDefault.Foreground(tcell.ColorRed),
		ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
		ctx.Printf(0, 0, styleError,
			"No filter configured for this mimetype ('%s/%s')",
			pv.part.MIMEType, pv.part.MIMESubType,
		)
		ctx.Printf(0, 2, tcell.StyleDefault,
		ctx.Printf(0, 2, style,
			"You can still :save the message or :pipe it to an external command")
		pv.selecter.Focus(true)
		pv.grid.Draw(ctx)
@@ -657,8 +673,8 @@ func (pv *PartViewer) Draw(ctx *ui.Context) {
		pv.fetched = true
	}
	if pv.err != nil {
		ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
		ctx.Printf(0, 0, tcell.StyleDefault, "%s", pv.err.Error())
		ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
		ctx.Printf(0, 0, style, "%s", pv.err.Error())
		return
	}
	pv.term.Draw(ctx)
@@ -680,8 +696,10 @@ func (pv *PartViewer) Event(event tcell.Event) bool {

type HeaderView struct {
	ui.Invalidatable
	Name  string
	Value string
	conf     *config.AercConfig
	Name     string
	Value    string
	uiConfig config.UIConfig
}

func (hv *HeaderView) Draw(ctx *ui.Context) {
@@ -689,18 +707,15 @@ func (hv *HeaderView) Draw(ctx *ui.Context) {
	size := runewidth.StringWidth(name)
	lim := ctx.Width() - size - 1
	value := runewidth.Truncate(" "+hv.Value, lim, "…")
	var (
		hstyle tcell.Style
		vstyle tcell.Style
	)

	vstyle := hv.uiConfig.GetStyle(config.STYLE_DEFAULT)
	hstyle := hv.uiConfig.GetStyle(config.STYLE_HEADER)

	// TODO: Make this more robust and less dumb
	if hv.Name == "PGP" {
		vstyle = tcell.StyleDefault.Foreground(tcell.ColorGreen)
		hstyle = tcell.StyleDefault.Bold(true)
	} else {
		vstyle = tcell.StyleDefault
		hstyle = tcell.StyleDefault.Bold(true)
		vstyle = hv.uiConfig.GetStyle(config.STYLE_SUCCESS)
	}

	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle)
	ctx.Printf(0, 0, hstyle, "%s", name)
	ctx.Printf(size, 0, vstyle, "%s", value)
diff --git a/widgets/pgpinfo.go b/widgets/pgpinfo.go
index 5da9141..94fb877 100644
--- a/widgets/pgpinfo.go
+++ b/widgets/pgpinfo.go
@@ -3,40 +3,40 @@ package widgets
import (
	"errors"

	"git.sr.ht/~sircmpwn/aerc/config"
	"git.sr.ht/~sircmpwn/aerc/lib/ui"

	"github.com/gdamore/tcell"
	"golang.org/x/crypto/openpgp"
	pgperrors "golang.org/x/crypto/openpgp/errors"
)

type PGPInfo struct {
	ui.Invalidatable
	details *openpgp.MessageDetails
	details  *openpgp.MessageDetails
	uiConfig config.UIConfig
}

func NewPGPInfo(details *openpgp.MessageDetails) *PGPInfo {
	return &PGPInfo{details: details}
func NewPGPInfo(details *openpgp.MessageDetails, uiConfig config.UIConfig) *PGPInfo {
	return &PGPInfo{details: details, uiConfig: uiConfig}
}

func (p *PGPInfo) DrawSignature(ctx *ui.Context) {
	errorStyle := tcell.StyleDefault.Background(tcell.ColorRed).
		Foreground(tcell.ColorWhite).Bold(true)
	softErrorStyle := tcell.StyleDefault.Foreground(tcell.ColorYellow).Bold(true)
	validStyle := tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true)
	errorStyle := p.uiConfig.GetStyle(config.STYLE_ERROR)
	warningStyle := p.uiConfig.GetStyle(config.STYLE_WARNING)
	validStyle := p.uiConfig.GetStyle(config.STYLE_SUCCESS)
	defaultStyle := p.uiConfig.GetStyle(config.STYLE_DEFAULT)

	// TODO: Nicer prompt for TOFU, fetch from keyserver, etc
	if errors.Is(p.details.SignatureError, pgperrors.ErrUnknownIssuer) ||
		p.details.SignedBy == nil {

		x := ctx.Printf(0, 0, softErrorStyle, "*")
		x += ctx.Printf(x, 0, tcell.StyleDefault,
		x := ctx.Printf(0, 0, warningStyle, "*")
		x += ctx.Printf(x, 0, defaultStyle,
			" Signed with unknown key (%8X); authenticity unknown",
			p.details.SignedByKeyId)
	} else if p.details.SignatureError != nil {
		x := ctx.Printf(0, 0, errorStyle, "Invalid signature!")
		x += ctx.Printf(x, 0, tcell.StyleDefault.
			Foreground(tcell.ColorRed).Bold(true),
		x += ctx.Printf(x, 0, errorStyle,
			" This message may have been tampered with! (%s)",
			p.details.SignatureError.Error())
	} else {
@@ -44,24 +44,26 @@ func (p *PGPInfo) DrawSignature(ctx *ui.Context) {
		ident := entity.PrimaryIdentity()

		x := ctx.Printf(0, 0, validStyle, "✓ Authentic ")
		x += ctx.Printf(x, 0, tcell.StyleDefault,
		x += ctx.Printf(x, 0, defaultStyle,
			"Signature from %s (%8X)",
			ident.Name, p.details.SignedByKeyId)
	}
}

func (p *PGPInfo) DrawEncryption(ctx *ui.Context, y int) {
	validStyle := tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true)
	validStyle := p.uiConfig.GetStyle(config.STYLE_SUCCESS)
	defaultStyle := p.uiConfig.GetStyle(config.STYLE_DEFAULT)
	entity := p.details.DecryptedWith.Entity
	ident := entity.PrimaryIdentity()

	x := ctx.Printf(0, y, validStyle, "✓ Encrypted ")
	x += ctx.Printf(x, y, tcell.StyleDefault,
	x += ctx.Printf(x, y, defaultStyle,
		"To %s (%8X) ", ident.Name, p.details.DecryptedWith.PublicKey.KeyId)
}

func (p *PGPInfo) Draw(ctx *ui.Context) {
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
	defaultStyle := p.uiConfig.GetStyle(config.STYLE_DEFAULT)
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle)
	if p.details.IsSigned && p.details.IsEncrypted {
		p.DrawSignature(ctx)
		p.DrawEncryption(ctx, 1)
diff --git a/widgets/selecter.go b/widgets/selecter.go
index 7fae9cd..d68a8cc 100644
--- a/widgets/selecter.go
+++ b/widgets/selecter.go
@@ -3,24 +3,27 @@ package widgets
import (
	"github.com/gdamore/tcell"

	"git.sr.ht/~sircmpwn/aerc/config"
	"git.sr.ht/~sircmpwn/aerc/lib/ui"
)

type Selecter struct {
	ui.Invalidatable
	chooser bool
	focused bool
	focus   int
	options []string
	chooser  bool
	focused  bool
	focus    int
	options  []string
	uiConfig config.UIConfig

	onChoose func(option string)
	onSelect func(option string)
}

func NewSelecter(options []string, focus int) *Selecter {
func NewSelecter(options []string, focus int, uiConfig config.UIConfig) *Selecter {
	return &Selecter{
		focus:   focus,
		options: options,
		focus:    focus,
		options:  options,
		uiConfig: uiConfig,
	}
}

@@ -34,15 +37,16 @@ func (sel *Selecter) Invalidate() {
}

func (sel *Selecter) Draw(ctx *ui.Context) {
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
		sel.uiConfig.GetStyle(config.STYLE_SELECTOR_DEFAULT))
	x := 2
	for i, option := range sel.options {
		style := tcell.StyleDefault
		style := sel.uiConfig.GetStyle(config.STYLE_SELECTOR_DEFAULT)
		if sel.focus == i {
			if sel.focused {
				style = style.Reverse(true)
				style = sel.uiConfig.GetStyle(config.STYLE_SELECTOR_FOCUSED)
			} else if sel.chooser {
				style = style.Bold(true)
				style = sel.uiConfig.GetStyle(config.STYLE_SELECTOR_CHOOSER)
			}
		}
		x += ctx.Printf(x, 1, style, "[%s]", option)
diff --git a/widgets/spinner.go b/widgets/spinner.go
index 51b8c1b..0c72422 100644
--- a/widgets/spinner.go
+++ b/widgets/spinner.go
@@ -16,6 +16,7 @@ type Spinner struct {
	frame  int64 // access via atomic
	frames []string
	stop   chan struct{}
	style  tcell.Style
}

func NewSpinner(uiConf *config.UIConfig) *Spinner {
@@ -23,6 +24,7 @@ func NewSpinner(uiConf *config.UIConfig) *Spinner {
		stop:   make(chan struct{}),
		frame:  -1,
		frames: strings.Split(uiConf.Spinner, uiConf.SpinnerDelimiter),
		style:  uiConf.GetStyle(config.STYLE_SPINNER),
	}
	return &spinner
}
@@ -70,9 +72,9 @@ func (s *Spinner) Draw(ctx *ui.Context) {

	cur := int(atomic.LoadInt64(&s.frame) % int64(len(s.frames)))

	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', s.style)
	col := ctx.Width()/2 - len(s.frames[0])/2 + 1
	ctx.Printf(col, 0, tcell.StyleDefault, "%s", s.frames[cur])
	ctx.Printf(col, 0, s.style, "%s", s.frames[cur])
}

func (s *Spinner) Invalidate() {
diff --git a/widgets/status.go b/widgets/status.go
index 6bdeb4f..e3272fe 100644
--- a/widgets/status.go
+++ b/widgets/status.go
@@ -6,6 +6,7 @@ import (
	"github.com/gdamore/tcell"
	"github.com/mattn/go-runewidth"

	"git.sr.ht/~sircmpwn/aerc/config"
	"git.sr.ht/~sircmpwn/aerc/lib/ui"
)

@@ -14,21 +15,21 @@ type StatusLine struct {
	stack    []*StatusMessage
	fallback StatusMessage
	aerc     *Aerc
	uiConfig config.UIConfig
}

type StatusMessage struct {
	bg      tcell.Color
	fg      tcell.Color
	style   tcell.Style
	message string
}

func NewStatusLine() *StatusLine {
func NewStatusLine(uiConfig config.UIConfig) *StatusLine {
	return &StatusLine{
		fallback: StatusMessage{
			bg:      tcell.ColorDefault,
			fg:      tcell.ColorDefault,
			style:   uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT),
			message: "Idle",
		},
		uiConfig: uiConfig,
	}
}

@@ -41,9 +42,7 @@ func (status *StatusLine) Draw(ctx *ui.Context) {
	if len(status.stack) != 0 {
		line = status.stack[len(status.stack)-1]
	}
	style := tcell.StyleDefault.
		Background(line.bg).Foreground(line.fg).Reverse(true)
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', line.style)
	pendingKeys := ""
	if status.aerc != nil {
		for _, pendingKey := range status.aerc.pendingKeys {
@@ -51,13 +50,21 @@ func (status *StatusLine) Draw(ctx *ui.Context) {
		}
	}
	message := runewidth.FillRight(line.message, ctx.Width()-len(pendingKeys)-5)
	ctx.Printf(0, 0, style, "%s%s", message, pendingKeys)
	ctx.Printf(0, 0, line.style, "%s%s", message, pendingKeys)
}

func (status *StatusLine) Set(text string) *StatusMessage {
	status.fallback = StatusMessage{
		bg:      tcell.ColorDefault,
		fg:      tcell.ColorDefault,
		style:   status.uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT),
		message: text,
	}
	status.Invalidate()
	return &status.fallback
}

func (status *StatusLine) SetError(text string) *StatusMessage {
	status.fallback = StatusMessage{
		style:   status.uiConfig.GetStyle(config.STYLE_STATUSLINE_ERROR),
		message: text,
	}
	status.Invalidate()
@@ -66,8 +73,7 @@ func (status *StatusLine) Set(text string) *StatusMessage {

func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage {
	msg := &StatusMessage{
		bg:      tcell.ColorDefault,
		fg:      tcell.ColorDefault,
		style:   status.uiConfig.GetStyle(config.STYLE_STATUSLINE_DEFAULT),
		message: text,
	}
	status.stack = append(status.stack, msg)
@@ -85,6 +91,18 @@ func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage
	return msg
}

func (status *StatusLine) PushError(text string, expiry time.Duration) *StatusMessage {
	msg := status.Push(text, expiry)
	msg.Color(status.uiConfig.GetStyle(config.STYLE_STATUSLINE_ERROR))
	return msg
}

func (status *StatusLine) PushSuccess(text string, expiry time.Duration) *StatusMessage {
	msg := status.Push(text, expiry)
	msg.Color(status.uiConfig.GetStyle(config.STYLE_STATUSLINE_SUCCESS))
	return msg
}

func (status *StatusLine) Expire() {
	status.stack = nil
}
@@ -93,7 +111,6 @@ func (status *StatusLine) SetAerc(aerc *Aerc) {
	status.aerc = aerc
}

func (msg *StatusMessage) Color(bg tcell.Color, fg tcell.Color) {
	msg.bg = bg
	msg.fg = fg
func (msg *StatusMessage) Color(style tcell.Style) {
	msg.style = style
}
diff --git a/widgets/tabhost.go b/widgets/tabhost.go
index 0ac67e5..1322a0a 100644
--- a/widgets/tabhost.go
+++ b/widgets/tabhost.go
@@ -7,6 +7,9 @@ import (
type TabHost interface {
	BeginExCommand(cmd string)
	SetStatus(status string) *StatusMessage
	SetError(err string) *StatusMessage
	PushStatus(text string, expiry time.Duration) *StatusMessage
	PushError(text string, expiry time.Duration) *StatusMessage
	PushSuccess(text string, expiry time.Duration) *StatusMessage
	Beep()
}
-- 
2.27.0

[PATCH v4 2/3] Rename selecter to selector Export this patch

diff --git a/doc/aerc-stylesets.7.scd b/doc/aerc-stylesets.7.scd
index 829418e..fd7732d 100644
--- a/doc/aerc-stylesets.7.scd
+++ b/doc/aerc-stylesets.7.scd
@@ -129,12 +129,12 @@ styling.
:  The style for the loading spinner.
|  border
:  The style used to draw borders. *Only the background color is used*.
|  selecter_default
:  The default style for the selecter ui element.
|  selecter_focused
:  The focused item in a selecter ui element.
|  selecter_chooser
:  The item chooser in a selecter ui element.
|  selector_default
:  The default style for the selector ui element.
|  selector_focused
:  The focused item in a selector ui element.
|  selector_chooser
:  The item chooser in a selector ui element.

## fnmatch style wildcard matching
The styleset configuration can be made simpler by using the fnmatch
diff --git a/widgets/account-wizard.go b/widgets/account-wizard.go
index 9e3db11..1921f06 100644
--- a/widgets/account-wizard.go
+++ b/widgets/account-wizard.go
@@ -180,7 +180,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
		At(7, 0)
	basics.AddChild(wizard.email).
		At(8, 0)
	selecter := NewSelecter([]string{"Next"}, 0, conf.Ui).
	selector := NewSelector([]string{"Next"}, 0, conf.Ui).
		OnChoose(func(option string) {
			email := wizard.email.String()
			if strings.ContainsRune(email, '@') {
@@ -204,9 +204,9 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
			}
			wizard.advance(option)
		})
	basics.AddChild(selecter).At(9, 0)
	basics.AddChild(selector).At(9, 0)
	wizard.basics = []ui.Interactive{
		wizard.accountName, wizard.fullName, wizard.email, selecter,
		wizard.accountName, wizard.fullName, wizard.email, selector,
	}
	basics.OnInvalidate(func(_ ui.Drawable) {
		wizard.Invalidate()
@@ -262,7 +262,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
		ui.NewText("Connection mode",
			conf.Ui.GetStyle(config.STYLE_HEADER))).
		At(10, 0)
	imapMode := NewSelecter([]string{
	imapMode := NewSelector([]string{
		"IMAP over SSL/TLS",
		"IMAP with STARTTLS",
		"Insecure IMAP",
@@ -278,14 +278,14 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
		wizard.imapUri()
	})
	incoming.AddChild(imapMode).At(11, 0)
	selecter = NewSelecter([]string{"Previous", "Next"}, 1, conf.Ui).
	selector = NewSelector([]string{"Previous", "Next"}, 1, conf.Ui).
		OnChoose(wizard.advance)
	incoming.AddChild(ui.NewFill(' ')).At(12, 0)
	incoming.AddChild(wizard.imapStr).At(13, 0)
	incoming.AddChild(selecter).At(14, 0)
	incoming.AddChild(selector).At(14, 0)
	wizard.incoming = []ui.Interactive{
		wizard.imapUsername, wizard.imapPassword, wizard.imapServer,
		imapMode, selecter,
		imapMode, selector,
	}
	incoming.OnInvalidate(func(_ ui.Drawable) {
		wizard.Invalidate()
@@ -344,7 +344,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
		ui.NewText("Connection mode",
			conf.Ui.GetStyle(config.STYLE_HEADER))).
		At(10, 0)
	smtpMode := NewSelecter([]string{
	smtpMode := NewSelector([]string{
		"SMTP over SSL/TLS",
		"SMTP with STARTTLS",
		"Insecure SMTP",
@@ -360,7 +360,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
		wizard.smtpUri()
	})
	outgoing.AddChild(smtpMode).At(11, 0)
	selecter = NewSelecter([]string{"Previous", "Next"}, 1, conf.Ui).
	selector = NewSelector([]string{"Previous", "Next"}, 1, conf.Ui).
		OnChoose(wizard.advance)
	outgoing.AddChild(ui.NewFill(' ')).At(12, 0)
	outgoing.AddChild(wizard.smtpStr).At(13, 0)
@@ -368,7 +368,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
	outgoing.AddChild(
		ui.NewText("Copy sent messages to 'Sent' folder?",
			conf.Ui.GetStyle(config.STYLE_HEADER))).At(15, 0)
	copySent := NewSelecter([]string{"Yes", "No"}, 0, conf.Ui).
	copySent := NewSelector([]string{"Yes", "No"}, 0, conf.Ui).
		Chooser(true).OnChoose(func(option string) {
		switch option {
		case "Yes":
@@ -378,10 +378,10 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
		}
	})
	outgoing.AddChild(copySent).At(16, 0)
	outgoing.AddChild(selecter).At(17, 0)
	outgoing.AddChild(selector).At(17, 0)
	wizard.outgoing = []ui.Interactive{
		wizard.smtpUsername, wizard.smtpPassword, wizard.smtpServer,
		smtpMode, copySent, selecter,
		smtpMode, copySent, selector,
	}
	outgoing.OnInvalidate(func(_ ui.Drawable) {
		wizard.Invalidate()
@@ -399,7 +399,7 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
			"save your settings to accounts.conf.\n\n"+
			"To add another account in the future, run ':new-account'.",
		conf.Ui.GetStyle(config.STYLE_DEFAULT)))
	selecter = NewSelecter([]string{
	selector = NewSelector([]string{
		"Previous",
		"Finish & open tutorial",
		"Finish",
@@ -413,8 +413,8 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
			wizard.finish(false)
		}
	})
	complete.AddChild(selecter).At(1, 0)
	wizard.complete = []ui.Interactive{selecter}
	complete.AddChild(selector).At(1, 0)
	wizard.complete = []ui.Interactive{selector}
	complete.OnInvalidate(func(_ ui.Drawable) {
		wizard.Invalidate()
	})
diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go
index 29e07b2..ab6c07e 100644
--- a/widgets/msgviewer.go
+++ b/widgets/msgviewer.go
@@ -457,7 +457,7 @@ type PartViewer struct {
	sink        io.WriteCloser
	source      io.Reader
	term        *Terminal
	selecter    *Selecter
	selector    *Selector
	grid        *ui.Grid
	uiConfig    config.UIConfig
}
@@ -529,7 +529,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
		{ui.SIZE_WEIGHT, ui.Const(1)},
	})

	selecter := NewSelecter([]string{"Save message", "Pipe to command"},
	selector := NewSelector([]string{"Save message", "Pipe to command"},
		0, acct.UiConfig()).
		OnChoose(func(option string) {
			switch option {
@@ -540,7 +540,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
			}
		})

	grid.AddChild(selecter).At(2, 0)
	grid.AddChild(selector).At(2, 0)

	pv := &PartViewer{
		conf:        conf,
@@ -553,7 +553,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
		showHeaders: conf.Viewer.ShowHeaders,
		sink:        pipe,
		term:        term,
		selecter:    selecter,
		selector:    selector,
		grid:        grid,
		uiConfig:    acct.UiConfig(),
	}
@@ -664,7 +664,7 @@ func (pv *PartViewer) Draw(ctx *ui.Context) {
		)
		ctx.Printf(0, 2, style,
			"You can still :save the message or :pipe it to an external command")
		pv.selecter.Focus(true)
		pv.selector.Focus(true)
		pv.grid.Draw(ctx)
		return
	}
@@ -691,7 +691,7 @@ func (pv *PartViewer) Event(event tcell.Event) bool {
	if pv.term != nil {
		return pv.term.Event(event)
	}
	return pv.selecter.Event(event)
	return pv.selector.Event(event)
}

type HeaderView struct {
-- 
2.27.0

[PATCH v4 3/3] Fix a couple of issues with the style patch. Export this patch

The following things were changed:
* Struct tags separated by space
* Don't hardcode default styleset, just try to load default location.
* Remove expiry duration from PushSuccess and PushError - hard code.
* Formatting.
---
 commands/account/mkdir.go            |  2 +-
 commands/account/view.go             |  3 +--
 commands/compose/attach.go           |  9 +++----
 commands/compose/detach.go           |  3 +--
 commands/compose/postpone.go         |  6 ++---
 commands/compose/send.go             |  4 +--
 commands/exec.go                     |  4 +--
 commands/msg/archive.go              |  2 +-
 commands/msg/copy.go                 |  2 +-
 commands/msg/delete.go               |  6 ++---
 commands/msg/forward.go              |  3 +--
 commands/msg/modify-labels.go        |  2 +-
 commands/msg/move.go                 |  2 +-
 commands/msg/pipe.go                 |  6 ++---
 commands/msg/read.go                 |  2 +-
 commands/msg/recall.go               |  3 +--
 commands/msg/reply.go                |  3 +--
 commands/msgview/next.go             |  4 +--
 commands/msgview/open.go             |  6 ++---
 commands/msgview/save.go             |  2 +-
 commands/term.go                     |  3 +--
 commands/util.go                     |  4 +--
 config/config.go                     | 19 ++------------
 config/style.go                      | 37 ----------------------------
 widgets/account-wizard.go            |  7 +++---
 widgets/account.go                   |  5 ++--
 widgets/aerc.go                      | 16 ++++++------
 widgets/compose.go                   |  5 ++--
 widgets/getpasswd.go                 |  3 ++-
 widgets/msglist.go                   |  3 +--
 widgets/{selecter.go => selector.go} | 22 ++++++++---------
 widgets/status.go                    |  8 +++---
 widgets/tabhost.go                   |  4 +--
 33 files changed, 73 insertions(+), 137 deletions(-)
 rename widgets/{selecter.go => selector.go} (75%)

diff --git a/commands/account/mkdir.go b/commands/account/mkdir.go
index f99fc01..9d16063 100644
--- a/commands/account/mkdir.go
+++ b/commands/account/mkdir.go
@@ -40,7 +40,7 @@ func (MakeDir) Execute(aerc *widgets.Aerc, args []string) error {
			aerc.PushStatus("Directory created.", 10*time.Second)
			acct.Directories().Select(name)
		case *types.Error:
			aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
			aerc.PushError(" "+msg.Error.Error())
		}
	})
	return nil
diff --git a/commands/account/view.go b/commands/account/view.go
index d4653be..b421666 100644
--- a/commands/account/view.go
+++ b/commands/account/view.go
@@ -2,7 +2,6 @@ package account

import (
	"errors"
	"time"

	"git.sr.ht/~sircmpwn/aerc/lib"
	"git.sr.ht/~sircmpwn/aerc/widgets"
@@ -42,7 +41,7 @@ func (ViewMessage) Execute(aerc *widgets.Aerc, args []string) error {
	lib.NewMessageStoreView(msg, store, aerc.DecryptKeys,
		func(view lib.MessageView, err error) {
			if err != nil {
				aerc.PushError(err.Error(), 10*time.Second)
				aerc.PushError(err.Error())
				return
			}
			viewer := widgets.NewMessageViewer(acct, aerc.Config(), view)
diff --git a/commands/compose/attach.go b/commands/compose/attach.go
index 6b8d72f..294f1b1 100644
--- a/commands/compose/attach.go
+++ b/commands/compose/attach.go
@@ -4,7 +4,6 @@ import (
	"fmt"
	"os"
	"strings"
	"time"

	"git.sr.ht/~sircmpwn/aerc/commands"
	"git.sr.ht/~sircmpwn/aerc/widgets"
@@ -35,23 +34,23 @@ func (Attach) Execute(aerc *widgets.Aerc, args []string) error {

	path, err := homedir.Expand(path)
	if err != nil {
		aerc.PushError(" "+err.Error(), 10*time.Second)
		aerc.PushError(" "+err.Error())
		return err
	}

	pathinfo, err := os.Stat(path)
	if err != nil {
		aerc.PushError(" "+err.Error(), 10*time.Second)
		aerc.PushError(" "+err.Error())
		return err
	} else if pathinfo.IsDir() {
		aerc.PushError("Attachment must be a file, not a directory", 10*time.Second)
		aerc.PushError("Attachment must be a file, not a directory")
		return nil
	}

	composer, _ := aerc.SelectedTab().(*widgets.Composer)
	composer.AddAttachment(path)

	aerc.PushSuccess(fmt.Sprintf("Attached %s", pathinfo.Name()), 10*time.Second)
	aerc.PushSuccess(fmt.Sprintf("Attached %s", pathinfo.Name()))

	return nil
}
diff --git a/commands/compose/detach.go b/commands/compose/detach.go
index 8bc0e88..b48159d 100644
--- a/commands/compose/detach.go
+++ b/commands/compose/detach.go
@@ -3,7 +3,6 @@ package compose
import (
	"fmt"
	"strings"
	"time"

	"git.sr.ht/~sircmpwn/aerc/widgets"
)
@@ -43,7 +42,7 @@ func (Detach) Execute(aerc *widgets.Aerc, args []string) error {
		return err
	}

	aerc.PushSuccess(fmt.Sprintf("Detached %s", path), 10*time.Second)
	aerc.PushSuccess(fmt.Sprintf("Detached %s", path))

	return nil
}
diff --git a/commands/compose/postpone.go b/commands/compose/postpone.go
index 90b6134..bdf3d48 100644
--- a/commands/compose/postpone.go
+++ b/commands/compose/postpone.go
@@ -63,7 +63,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error {
	go func() {
		errStr := <-errChan
		if errStr != "" {
			aerc.PushError(" "+errStr, 10*time.Second)
			aerc.PushError(" "+errStr)
			return
		}

@@ -71,7 +71,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error {
		ctr := datacounter.NewWriterCounter(ioutil.Discard)
		err = composer.WriteMessage(header, ctr)
		if err != nil {
			aerc.PushError(errors.Wrap(err, "WriteMessage").Error(), 10*time.Second)
			aerc.PushError(errors.Wrap(err, "WriteMessage").Error())
			composer.Close()
			return
		}
@@ -90,7 +90,7 @@ func (Postpone) Execute(aerc *widgets.Aerc, args []string) error {
				r.Close()
				composer.Close()
			case *types.Error:
				aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
				aerc.PushError(" "+msg.Error.Error())
				r.Close()
				composer.Close()
			}
diff --git a/commands/compose/send.go b/commands/compose/send.go
index cbcc876..1177371 100644
--- a/commands/compose/send.go
+++ b/commands/compose/send.go
@@ -224,7 +224,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
		aerc.PushStatus("Sending...", 10*time.Second)
		nbytes, err := sendAsync()
		if err != nil {
			aerc.PushError(" " + err.Error(), 10*time.Second)
			aerc.PushError(" " + err.Error())
			return
		}
		if config.CopyTo != "" {
@@ -245,7 +245,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
					composer.SetSent()
					composer.Close()
				case *types.Error:
					aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
					aerc.PushError(" "+msg.Error.Error())
					r.Close()
					composer.Close()
				}
diff --git a/commands/exec.go b/commands/exec.go
index 0a5470d..3dafe49 100644
--- a/commands/exec.go
+++ b/commands/exec.go
@@ -31,12 +31,12 @@ func (ExecCmd) Execute(aerc *widgets.Aerc, args []string) error {
	go func() {
		err := cmd.Run()
		if err != nil {
			aerc.PushError(" "+err.Error(), 10*time.Second)
			aerc.PushError(" "+err.Error())
		} else {
			if cmd.ProcessState.ExitCode() != 0 {
				aerc.PushError(fmt.Sprintf(
					"%s: completed with status %d", args[0],
					cmd.ProcessState.ExitCode()), 10*time.Second)
					cmd.ProcessState.ExitCode()))
			} else {
				aerc.PushStatus(fmt.Sprintf(
					"%s: completed with status %d", args[0],
diff --git a/commands/msg/archive.go b/commands/msg/archive.go
index 8d42308..1018fd7 100644
--- a/commands/msg/archive.go
+++ b/commands/msg/archive.go
@@ -86,7 +86,7 @@ func (Archive) Execute(aerc *widgets.Aerc, args []string) error {
			case *types.Done:
				wg.Done()
			case *types.Error:
				aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
				aerc.PushError(" "+msg.Error.Error())
				success = false
				wg.Done()
			}
diff --git a/commands/msg/copy.go b/commands/msg/copy.go
index e822c5c..49614f4 100644
--- a/commands/msg/copy.go
+++ b/commands/msg/copy.go
@@ -60,7 +60,7 @@ func (Copy) Execute(aerc *widgets.Aerc, args []string) error {
			case *types.Done:
				aerc.PushStatus("Messages copied.", 10*time.Second)
			case *types.Error:
				aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
				aerc.PushError(" "+msg.Error.Error())
			}
		})
	return nil
diff --git a/commands/msg/delete.go b/commands/msg/delete.go
index 26f0df8..6eb35eb 100644
--- a/commands/msg/delete.go
+++ b/commands/msg/delete.go
@@ -47,10 +47,10 @@ func (Delete) Execute(aerc *widgets.Aerc, args []string) error {
		case *types.Done:
			aerc.PushStatus("Messages deleted.", 10*time.Second)
		case *types.Error:
			aerc.PushError(" " + msg.Error.Error(), 10*time.Second)
			aerc.PushError(" " + msg.Error.Error())
		case *types.Unsupported:
			// notmuch doesn't support it, we want the user to know
			aerc.PushError(" error, unsupported for this worker", 10*time.Second)
			aerc.PushError(" error, unsupported for this worker")
		}
	})

@@ -71,7 +71,7 @@ func (Delete) Execute(aerc *widgets.Aerc, args []string) error {
			lib.NewMessageStoreView(next, store, aerc.DecryptKeys,
				func(view lib.MessageView, err error) {
					if err != nil {
						aerc.PushError(err.Error(), 10*time.Second)
						aerc.PushError(err.Error())
						return
					}
					nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view)
diff --git a/commands/msg/forward.go b/commands/msg/forward.go
index 61e3a24..ab55877 100644
--- a/commands/msg/forward.go
+++ b/commands/msg/forward.go
@@ -9,7 +9,6 @@ import (
	"os"
	"path"
	"strings"
	"time"

	"git.sr.ht/~sircmpwn/aerc/models"
	"git.sr.ht/~sircmpwn/aerc/widgets"
@@ -84,7 +83,7 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
		composer, err := widgets.NewComposer(aerc, acct, aerc.Config(), acct.AccountConfig(),
			acct.Worker(), template, defaults, original)
		if err != nil {
			aerc.PushError("Error: "+err.Error(), 10*time.Second)
			aerc.PushError("Error: "+err.Error())
			return nil, err
		}

diff --git a/commands/msg/modify-labels.go b/commands/msg/modify-labels.go
index d74aece..d68a895 100644
--- a/commands/msg/modify-labels.go
+++ b/commands/msg/modify-labels.go
@@ -58,7 +58,7 @@ func (ModifyLabels) Execute(aerc *widgets.Aerc, args []string) error {
		case *types.Done:
			aerc.PushStatus("labels updated", 10*time.Second)
		case *types.Error:
			aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
			aerc.PushError(" "+msg.Error.Error())
		}
	})
	return nil
diff --git a/commands/msg/move.go b/commands/msg/move.go
index 1a8f949..820217e 100644
--- a/commands/msg/move.go
+++ b/commands/msg/move.go
@@ -71,7 +71,7 @@ func (Move) Execute(aerc *widgets.Aerc, args []string) error {
		case *types.Done:
			aerc.PushStatus("Message moved to "+joinedArgs, 10*time.Second)
		case *types.Error:
			aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
			aerc.PushError(" "+msg.Error.Error())
		}
	})
	return nil
diff --git a/commands/msg/pipe.go b/commands/msg/pipe.go
index 5a0b120..2bc6b62 100644
--- a/commands/msg/pipe.go
+++ b/commands/msg/pipe.go
@@ -75,7 +75,7 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error {
	doTerm := func(reader io.Reader, name string) {
		term, err := commands.QuickTerm(aerc, cmd, reader)
		if err != nil {
			aerc.PushError(" "+err.Error(), 10*time.Second)
			aerc.PushError(" "+err.Error())
			return
		}
		aerc.NewTab(term, name)
@@ -93,12 +93,12 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error {
		}()
		err = ecmd.Run()
		if err != nil {
			aerc.PushError(" "+err.Error(), 10*time.Second)
			aerc.PushError(" "+err.Error())
		} else {
			if ecmd.ProcessState.ExitCode() != 0 {
				aerc.PushError(fmt.Sprintf(
					"%s: completed with status %d", cmd[0],
					ecmd.ProcessState.ExitCode()), 10*time.Second)
					ecmd.ProcessState.ExitCode()))
			} else {
				aerc.PushStatus(fmt.Sprintf(
					"%s: completed with status %d", cmd[0],
diff --git a/commands/msg/read.go b/commands/msg/read.go
index a0b68bc..21d7673 100644
--- a/commands/msg/read.go
+++ b/commands/msg/read.go
@@ -187,7 +187,7 @@ func submitFlagChange(aerc *widgets.Aerc, store *lib.MessageStore,
		case *types.Done:
			wg.Done()
		case *types.Error:
			aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
			aerc.PushError(" "+msg.Error.Error())
			*success = false
			wg.Done()
		}
diff --git a/commands/msg/recall.go b/commands/msg/recall.go
index 6c5e973..f24b5df 100644
--- a/commands/msg/recall.go
+++ b/commands/msg/recall.go
@@ -2,7 +2,6 @@ package msg

import (
	"io"
	"time"

	"github.com/emersion/go-message"
	_ "github.com/emersion/go-message/charset"
@@ -92,7 +91,7 @@ func (Recall) Execute(aerc *widgets.Aerc, args []string) error {
			}, func(msg types.WorkerMessage) {
				switch msg := msg.(type) {
				case *types.Error:
					aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
					aerc.PushError(" "+msg.Error.Error())
					composer.Close()
				}
			})
diff --git a/commands/msg/reply.go b/commands/msg/reply.go
index 1655052..3340b2f 100644
--- a/commands/msg/reply.go
+++ b/commands/msg/reply.go
@@ -7,7 +7,6 @@ import (
	"io"
	gomail "net/mail"
	"strings"
	"time"

	"git.sr.ht/~sircmpwn/getopt"

@@ -140,7 +139,7 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {
		composer, err := widgets.NewComposer(aerc, acct, aerc.Config(),
			acct.AccountConfig(), acct.Worker(), template, defaults, original)
		if err != nil {
			aerc.PushError("Error: "+err.Error(), 10*time.Second)
			aerc.PushError("Error: "+err.Error())
			return err
		}

diff --git a/commands/msgview/next.go b/commands/msgview/next.go
index f9fb3d7..978cf10 100644
--- a/commands/msgview/next.go
+++ b/commands/msgview/next.go
@@ -1,8 +1,6 @@
package msgview

import (
	"time"

	"git.sr.ht/~sircmpwn/aerc/commands/account"
	"git.sr.ht/~sircmpwn/aerc/lib"
	"git.sr.ht/~sircmpwn/aerc/widgets"
@@ -42,7 +40,7 @@ func (NextPrevMsg) Execute(aerc *widgets.Aerc, args []string) error {
	lib.NewMessageStoreView(nextMsg, store, aerc.DecryptKeys,
		func(view lib.MessageView, err error) {
			if err != nil {
				aerc.PushError(err.Error(), 10*time.Second)
				aerc.PushError(err.Error())
				return
			}
			nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view)
diff --git a/commands/msgview/open.go b/commands/msgview/open.go
index b4dfab7..c681f86 100644
--- a/commands/msgview/open.go
+++ b/commands/msgview/open.go
@@ -49,19 +49,19 @@ func (Open) Execute(aerc *widgets.Aerc, args []string) error {

		tmpFile, err := ioutil.TempFile(os.TempDir(), "aerc-*"+extension)
		if err != nil {
			aerc.PushError(" "+err.Error(), 10*time.Second)
			aerc.PushError(" "+err.Error())
			return
		}
		defer tmpFile.Close()

		_, err = io.Copy(tmpFile, reader)
		if err != nil {
			aerc.PushError(" "+err.Error(), 10*time.Second)
			aerc.PushError(" "+err.Error())
			return
		}

		lib.OpenFile(tmpFile.Name(), func(err error) {
			aerc.PushError(" " + err.Error(), 10*time.Second)
			aerc.PushError(" " + err.Error())
		})

		aerc.PushStatus("Opened", 10*time.Second)
diff --git a/commands/msgview/save.go b/commands/msgview/save.go
index ea1b8f3..ef6bba8 100644
--- a/commands/msgview/save.go
+++ b/commands/msgview/save.go
@@ -128,7 +128,7 @@ func (Save) Execute(aerc *widgets.Aerc, args []string) error {
	go func() {
		err := <-ch
		if err != nil {
			aerc.PushError(fmt.Sprintf("Save failed: %v", err), 10*time.Second)
			aerc.PushError(fmt.Sprintf("Save failed: %v", err))
			return
		}
		aerc.PushStatus("Saved to "+path, 10*time.Second)
diff --git a/commands/term.go b/commands/term.go
index 9023285..c75fda5 100644
--- a/commands/term.go
+++ b/commands/term.go
@@ -2,7 +2,6 @@ package commands

import (
	"os/exec"
	"time"

	"github.com/riywo/loginshell"

@@ -47,7 +46,7 @@ func TermCore(aerc *widgets.Aerc, args []string) error {
	term.OnClose = func(err error) {
		aerc.RemoveTab(term)
		if err != nil {
			aerc.PushError(" "+err.Error(), 10*time.Second)
			aerc.PushError(" "+err.Error())
		}
	}
	return nil
diff --git a/commands/util.go b/commands/util.go
index 7c7b6ab..011f419 100644
--- a/commands/util.go
+++ b/commands/util.go
@@ -32,7 +32,7 @@ func QuickTerm(aerc *widgets.Aerc, args []string, stdin io.Reader) (*widgets.Ter

	term.OnClose = func(err error) {
		if err != nil {
			aerc.PushError(" "+err.Error(), 10*time.Second)
			aerc.PushError(" "+err.Error())
			// remove the tab on error, otherwise it gets stuck
			aerc.RemoveTab(term)
		} else {
@@ -56,7 +56,7 @@ func QuickTerm(aerc *widgets.Aerc, args []string, stdin io.Reader) (*widgets.Ter

		err := <-status
		if err != nil {
			aerc.PushError(" "+err.Error(), 10*time.Second)
			aerc.PushError(" "+err.Error())
		}
	}

diff --git a/config/config.go b/config/config.go
index 0ccf350..7cd0964 100644
--- a/config/config.go
+++ b/config/config.go
@@ -45,7 +45,7 @@ type UIConfig struct {
	NextMessageOnDelete bool          `ini:"next-message-on-delete"`
	CompletionDelay     time.Duration `ini:"completion-delay"`
	CompletionPopovers  bool          `ini:"completion-popovers"`
	StyleSetDirs        []string      `ini:"stylesets-dirs", delim:":"`
	StyleSetDirs        []string      `ini:"stylesets-dirs" delim:":"`
	StyleSetName        string        `ini:"styleset-name"`
	style               StyleSet
}
@@ -340,11 +340,6 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
		if err := ui.MapTo(&config.Ui); err != nil {
			return err
		}

		stylesetsDirs := ui.Key("stylesets-dirs").String()
		if stylesetsDirs != "" {
			config.Ui.StyleSetDirs = strings.Split(stylesetsDirs, ":")
		}
	}
	for _, sectionName := range file.SectionStrings() {
		if !strings.Contains(sectionName, "ui:") {
@@ -359,10 +354,6 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
		if err := uiSection.MapTo(&uiSubConfig); err != nil {
			return err
		}
		stylesetsDirs := uiSection.Key("stylesets-dirs").String()
		if stylesetsDirs != "" {
			uiSubConfig.StyleSetDirs = strings.Split(stylesetsDirs, ":")
		}
		contextualUi :=
			UIConfigContext{
				UiConfig: uiSubConfig,
@@ -650,13 +641,7 @@ func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error {
	ui.style = NewStyleSet()
    err := ui.style.LoadStyleSet(ui.StyleSetName, styleSetDirs)
    if err != nil {
        fmt.Errorf("Error while parsing styleset \"%s\": %s", ui.StyleSetName, err)
        fmt.Printf("Loading default styles")

        err = ui.style.LoadDefaultStyleSet()
        if err != nil {
            return fmt.Errorf("Error while loading default styleset: %s", err)
        }
        return fmt.Errorf("Unable to load default styleset: %s", err)
    }

	return nil
diff --git a/config/style.go b/config/style.go
index dfedf35..7fbc1b1 100644
--- a/config/style.go
+++ b/config/style.go
@@ -95,34 +95,6 @@ type Style struct {
	Reverse   bool
}

var StyleDefaults = []byte(`
*.default=true
*.selected.reverse=toggle

title.reverse=true
header.bold=true

*error.bold=true
error.fg=red
warning.fg=yello
success.fg=green

statusline*.default=true
statusline_default.reverse=true
statusline_error.fg=red
statusline_error.reverse=true

msglist_unread.bold=true

completion_pill.reverse=true

tab.reverse=true
border.reverse=true

selector_focused.reverse=true
selector_chooser.bold=true
`)

func (s Style) Get() tcell.Style {
	return tcell.StyleDefault.
		Foreground(s.Fg).
@@ -386,15 +358,6 @@ func (ss *StyleSet) LoadStyleSet(stylesetName string, stylesetDirs []string) err
    return ss.ParseStyleSet(file)
}

func (ss *StyleSet) LoadDefaultStyleSet() error {
    file, err := ini.Load(StyleDefaults);
    if err != nil {
        return err
    }

    return ss.ParseStyleSet(file)
}

func fnmatchToRegex(pattern string) string {
	n := len(pattern)
	var regex strings.Builder
diff --git a/widgets/account-wizard.go b/widgets/account-wizard.go
index 1921f06..4d25cae 100644
--- a/widgets/account-wizard.go
+++ b/widgets/account-wizard.go
@@ -10,7 +10,6 @@ import (
	"path"
	"strconv"
	"strings"
	"time"

	"github.com/gdamore/tcell"
	"github.com/go-ini/ini"
@@ -429,7 +428,7 @@ func (wizard *AccountWizard) ConfigureTemporaryAccount(temporary bool) {

func (wizard *AccountWizard) errorFor(d ui.Interactive, err error) {
	if d == nil {
		wizard.aerc.PushError(" "+err.Error(), 10*time.Second)
		wizard.aerc.PushError(" "+err.Error())
		wizard.Invalidate()
		return
	}
@@ -444,7 +443,7 @@ func (wizard *AccountWizard) errorFor(d ui.Interactive, err error) {
				wizard.step = step
				wizard.focus = focus
				wizard.Focus(true)
				wizard.aerc.PushError(" "+err.Error(), 10*time.Second)
				wizard.aerc.PushError(" "+err.Error())
				wizard.Invalidate()
				return
			}
@@ -555,7 +554,7 @@ func (wizard *AccountWizard) finish(tutorial bool) {
		term.OnClose = func(err error) {
			wizard.aerc.RemoveTab(term)
			if err != nil {
				wizard.aerc.PushError(" "+err.Error(), 10*time.Second)
				wizard.aerc.PushError(" "+err.Error())
			}
		}
	}
diff --git a/widgets/account.go b/widgets/account.go
index 6567638..8d89eef 100644
--- a/widgets/account.go
+++ b/widgets/account.go
@@ -4,7 +4,6 @@ import (
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/gdamore/tcell"

@@ -273,7 +272,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
		acct.labels = msg.Labels
	case *types.Error:
		acct.logger.Printf("%v", msg.Error)
		acct.aerc.PushError(fmt.Sprintf("%v", msg.Error), 10*time.Second)
		acct.aerc.PushError(fmt.Sprintf("%v", msg.Error))
	}
}

@@ -283,7 +282,7 @@ func (acct *AccountView) getSortCriteria() []*types.SortCriterion {
	}
	criteria, err := sort.GetSortCriteria(acct.UiConfig().Sort)
	if err != nil {
		acct.aerc.PushError(" ui.sort: "+err.Error(), 10*time.Second)
		acct.aerc.PushError(" ui.sort: "+err.Error())
		return nil
	}
	return criteria
diff --git a/widgets/aerc.go b/widgets/aerc.go
index 367335d..ac532ac 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -390,12 +390,12 @@ func (aerc *Aerc) PushStatus(text string, expiry time.Duration) *StatusMessage {
	return aerc.statusline.Push(text, expiry)
}

func (aerc *Aerc) PushError(text string, expiry time.Duration) *StatusMessage {
	return aerc.statusline.PushError(text, expiry)
func (aerc *Aerc) PushError(text string) *StatusMessage {
	return aerc.statusline.PushError(text)
}

func (aerc *Aerc) PushSuccess(text string, expiry time.Duration) *StatusMessage {
	return aerc.statusline.PushSuccess(text, expiry)
func (aerc *Aerc) PushSuccess(text string) *StatusMessage {
	return aerc.statusline.PushSuccess(text)
}

func (aerc *Aerc) focus(item ui.Interactive) {
@@ -424,11 +424,11 @@ func (aerc *Aerc) BeginExCommand(cmd string) {
	exline := NewExLine(aerc.conf, cmd, func(cmd string) {
		parts, err := shlex.Split(cmd)
		if err != nil {
			aerc.PushError(" "+err.Error(), 10*time.Second)
			aerc.PushError(" "+err.Error())
		}
		err = aerc.cmd(parts)
		if err != nil {
			aerc.PushError(" "+err.Error(), 10*time.Second)
			aerc.PushError(" "+err.Error())
		}
		// only add to history if this is an unsimulated command,
		// ie one not executed from a keybinding
@@ -452,7 +452,7 @@ func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) {
		}
		err := aerc.cmd(cmd)
		if err != nil {
			aerc.PushError(" "+err.Error(), 10*time.Second)
			aerc.PushError(" "+err.Error())
		}
	}, func(cmd string) []string {
		return nil // TODO: completions
@@ -479,7 +479,7 @@ func (aerc *Aerc) RegisterChoices(choices []Choice) {
		}
		err := aerc.cmd(cmd)
		if err != nil {
			aerc.PushError(" "+err.Error(), 10*time.Second)
			aerc.PushError(" "+err.Error())
		}
	}, func(cmd string) []string {
		return nil // TODO: completions
diff --git a/widgets/compose.go b/widgets/compose.go
index 39e40cd..03c9175 100644
--- a/widgets/compose.go
+++ b/widgets/compose.go
@@ -73,7 +73,7 @@ func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
	templateData := templates.ParseTemplateData(defaults, original)
	cmpl := completer.New(conf.Compose.AddressBookCmd, func(err error) {
		aerc.PushError(
			fmt.Sprintf("could not complete header: %v", err), 10*time.Second)
			fmt.Sprintf("could not complete header: %v", err))
		worker.Logger.Printf("could not complete header: %v", err)
	}, aerc.Logger())
	layout, editors, focusable := buildComposeHeader(aerc, cmpl, defaults)
@@ -261,8 +261,7 @@ func (c *Composer) readSignatureFromFile() []byte {
	signature, err := ioutil.ReadFile(sigFile)
	if err != nil {
		c.aerc.PushError(
			fmt.Sprintf(" Error loading signature from file: %v", sigFile),
			10*time.Second)
			fmt.Sprintf(" Error loading signature from file: %v", sigFile))
		return nil
	}
	return signature
diff --git a/widgets/getpasswd.go b/widgets/getpasswd.go
index b3ea9e0..5a47094 100644
--- a/widgets/getpasswd.go
+++ b/widgets/getpasswd.go
@@ -18,7 +18,8 @@ type GetPasswd struct {
	conf     *config.AercConfig
}

func NewGetPasswd(title string, prompt string, conf *config.AercConfig, cb func(string, error)) *GetPasswd {
func NewGetPasswd(title string, prompt string, conf *config.AercConfig,
    cb func(string, error)) *GetPasswd {
	getpasswd := &GetPasswd{
		callback: cb,
		title:    title,
diff --git a/widgets/msglist.go b/widgets/msglist.go
index a98e79c..e38dd9e 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -4,7 +4,6 @@ import (
	"fmt"
	"log"
	"math"
	"time"

	"github.com/gdamore/tcell"
	"github.com/mattn/go-runewidth"
@@ -222,7 +221,7 @@ func (ml *MessageList) MouseEvent(localX int, localY int, event tcell.Event) {
				lib.NewMessageStoreView(msg, store, ml.aerc.DecryptKeys,
					func(view lib.MessageView, err error) {
						if err != nil {
							ml.aerc.PushError(err.Error(), 10*time.Second)
							ml.aerc.PushError(err.Error())
							return
						}
						viewer := NewMessageViewer(acct, ml.aerc.Config(), view)
diff --git a/widgets/selecter.go b/widgets/selector.go
similarity index 75%
rename from widgets/selecter.go
rename to widgets/selector.go
index d68a8cc..d19d38f 100644
--- a/widgets/selecter.go
+++ b/widgets/selector.go
@@ -7,7 +7,7 @@ import (
	"git.sr.ht/~sircmpwn/aerc/lib/ui"
)

type Selecter struct {
type Selector struct {
	ui.Invalidatable
	chooser  bool
	focused  bool
@@ -19,24 +19,24 @@ type Selecter struct {
	onSelect func(option string)
}

func NewSelecter(options []string, focus int, uiConfig config.UIConfig) *Selecter {
	return &Selecter{
func NewSelector(options []string, focus int, uiConfig config.UIConfig) *Selector {
	return &Selector{
		focus:    focus,
		options:  options,
		uiConfig: uiConfig,
	}
}

func (sel *Selecter) Chooser(chooser bool) *Selecter {
func (sel *Selector) Chooser(chooser bool) *Selector {
	sel.chooser = chooser
	return sel
}

func (sel *Selecter) Invalidate() {
func (sel *Selector) Invalidate() {
	sel.DoInvalidate(sel)
}

func (sel *Selecter) Draw(ctx *ui.Context) {
func (sel *Selector) Draw(ctx *ui.Context) {
	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
		sel.uiConfig.GetStyle(config.STYLE_SELECTOR_DEFAULT))
	x := 2
@@ -54,26 +54,26 @@ func (sel *Selecter) Draw(ctx *ui.Context) {
	}
}

func (sel *Selecter) OnChoose(fn func(option string)) *Selecter {
func (sel *Selector) OnChoose(fn func(option string)) *Selector {
	sel.onChoose = fn
	return sel
}

func (sel *Selecter) OnSelect(fn func(option string)) *Selecter {
func (sel *Selector) OnSelect(fn func(option string)) *Selector {
	sel.onSelect = fn
	return sel
}

func (sel *Selecter) Selected() string {
func (sel *Selector) Selected() string {
	return sel.options[sel.focus]
}

func (sel *Selecter) Focus(focus bool) {
func (sel *Selector) Focus(focus bool) {
	sel.focused = focus
	sel.Invalidate()
}

func (sel *Selecter) Event(event tcell.Event) bool {
func (sel *Selector) Event(event tcell.Event) bool {
	switch event := event.(type) {
	case *tcell.EventKey:
		switch event.Key() {
diff --git a/widgets/status.go b/widgets/status.go
index e3272fe..122ca5f 100644
--- a/widgets/status.go
+++ b/widgets/status.go
@@ -91,14 +91,14 @@ func (status *StatusLine) Push(text string, expiry time.Duration) *StatusMessage
	return msg
}

func (status *StatusLine) PushError(text string, expiry time.Duration) *StatusMessage {
	msg := status.Push(text, expiry)
func (status *StatusLine) PushError(text string) *StatusMessage {
	msg := status.Push(text, 10*time.Second)
	msg.Color(status.uiConfig.GetStyle(config.STYLE_STATUSLINE_ERROR))
	return msg
}

func (status *StatusLine) PushSuccess(text string, expiry time.Duration) *StatusMessage {
	msg := status.Push(text, expiry)
func (status *StatusLine) PushSuccess(text string) *StatusMessage {
	msg := status.Push(text, 10*time.Second)
	msg.Color(status.uiConfig.GetStyle(config.STYLE_STATUSLINE_SUCCESS))
	return msg
}
diff --git a/widgets/tabhost.go b/widgets/tabhost.go
index 1322a0a..28c9be0 100644
--- a/widgets/tabhost.go
+++ b/widgets/tabhost.go
@@ -9,7 +9,7 @@ type TabHost interface {
	SetStatus(status string) *StatusMessage
	SetError(err string) *StatusMessage
	PushStatus(text string, expiry time.Duration) *StatusMessage
	PushError(text string, expiry time.Duration) *StatusMessage
	PushSuccess(text string, expiry time.Duration) *StatusMessage
	PushError(text string) *StatusMessage
	PushSuccess(text string) *StatusMessage
	Beep()
}
-- 
2.27.0