~sircmpwn/aerc

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
2 2

[PATCH v6] Add Style configuration

Details
Message ID
<20200410195640.1005042-1-sri@vathsan.com>
DKIM signature
pass
Download raw message
Patch: +916 -337
The following functionalities are added to configure aerc ui styles.
- Read stylesets from file with very basic fnmatch wildcard matching
- Add default styleset
- Support different stylesets as part of UiConfig allowing contextual
  styles.
- Move widgets/ui elements to use the stylesets.
- Add configuration manual for the styleset
---
Alright, Sorry for the spam today. Found more time than I thought I
would. So lots of changes to this patch.
And it is becoming quite big. But I can't find more places
where I would have to add styling support. So things should stay as
is until I get feedback and I can a block of free time again :p

Anyway, have a nice weekend folks.

v6:
- "tab_default" was consistent and I had a mind fart.
- Loading styleset inside loadConfig enables runtime changes to
  stylesets
- Add Style support for borders
- Add Style support for statusline
- Use style_success for valid_PGP

 Makefile                      |   7 +-
 commands/account/mkdir.go     |   5 +-
 commands/compose/attach.go    |  10 +-
 commands/compose/detach.go    |   4 +-
 commands/compose/send.go      |   7 +-
 commands/exec.go              |  18 ++-
 commands/msg/archive.go       |   5 +-
 commands/msg/copy.go          |   4 +-
 commands/msg/delete.go        |   5 +-
 commands/msg/forward.go       |   3 +-
 commands/msg/modify-labels.go |   4 +-
 commands/msg/move.go          |   7 +-
 commands/msg/pipe.go          |  19 ++-
 commands/msg/read.go          |   8 +-
 commands/msg/reply.go         |   3 +-
 commands/msgview/open.go      |   6 +-
 commands/msgview/save.go      |   2 +-
 commands/term.go              |   4 +-
 commands/util.go              |   4 +-
 config/aerc.conf.in           |  11 ++
 config/config.go              |  59 ++++++++-
 config/default_styleset       |  39 ++++++
 config/style.go               | 240 ++++++++++++++++++++++++++++++++++
 doc/aerc-config.5.scd         |  14 ++
 doc/aerc-stylesets.7.scd      | 181 +++++++++++++++++++++++++
 lib/ui/borders.go             |  14 +-
 lib/ui/stack.go               |  10 +-
 lib/ui/tab.go                 |  11 +-
 lib/ui/text.go                |  42 +-----
 lib/ui/textinput.go           |  32 +++--
 widgets/account-wizard.go     | 111 +++++++++-------
 widgets/account.go            |  11 +-
 widgets/aerc.go               |  29 ++--
 widgets/compose.go            |  69 ++++++----
 widgets/dirlist.go            |  12 +-
 widgets/exline.go             |   6 +-
 widgets/getpasswd.go          |  18 ++-
 widgets/msglist.go            |  40 +++---
 widgets/msgviewer.go          |  63 +++++----
 widgets/pgpinfo.go            |  32 ++---
 widgets/selecter.go           |  26 ++--
 widgets/spinner.go            |   6 +-
 widgets/status.go             |  49 ++++---
 widgets/tabhost.go            |   3 +
 44 files changed, 916 insertions(+), 337 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 3f06c4da6bc3..5f037fd94801 100644
--- a/Makefile
+++ b/Makefile
@@ -36,7 +36,8 @@ DOCS := \
	aerc-notmuch.5 \
	aerc-smtp.5 \
	aerc-tutorial.7 \
	aerc-templates.7
	aerc-templates.7 \
	aerc-stylesets.7

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

install: all
	mkdir -m755 -p $(BINDIR) $(MANDIR)/man1 $(MANDIR)/man5 $(MANDIR)/man7 \
		$(SHAREDIR) $(SHAREDIR)/filters $(SHAREDIR)/templates
		$(SHAREDIR) $(SHAREDIR)/filters $(SHAREDIR)/templates $(SHAREDIR)/stylesets
	install -m755 aerc $(BINDIR)/aerc
	install -m644 aerc.1 $(MANDIR)/man1/aerc.1
	install -m644 aerc-search.1 $(MANDIR)/man1/aerc-search.1
@@ -71,6 +72,7 @@ install: all
	install -m644 aerc-smtp.5 $(MANDIR)/man5/aerc-smtp.5
	install -m644 aerc-tutorial.7 $(MANDIR)/man7/aerc-tutorial.7
	install -m644 aerc-templates.7 $(MANDIR)/man7/aerc-templates.7
	install -m644 aerc-stylesets.7 $(MANDIR)/man7/aerc-stylesets.7
	install -m644 config/accounts.conf $(SHAREDIR)/accounts.conf
	install -m644 aerc.conf $(SHAREDIR)/aerc.conf
	install -m644 config/binds.conf $(SHAREDIR)/binds.conf
@@ -79,6 +81,7 @@ install: all
	install -m755 filters/plaintext $(SHAREDIR)/filters/plaintext
	install -m644 templates/quoted_reply $(SHAREDIR)/templates/quoted_reply
	install -m644 templates/forward_as_body $(SHAREDIR)/templates/forward_as_body
	install -m644 config/default_styleset $(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 bb7e38a98c7d..f99fc01d1342 100644
--- a/commands/account/mkdir.go
+++ b/commands/account/mkdir.go
@@ -5,8 +5,6 @@ import (
	"strings"
	"time"

	"github.com/gdamore/tcell"

	"git.sr.ht/~sircmpwn/aerc/widgets"
	"git.sr.ht/~sircmpwn/aerc/worker/types"
)
@@ -42,8 +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.PushStatus(" "+msg.Error.Error(), 10*time.Second).
				Color(tcell.ColorDefault, tcell.ColorRed)
			aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
		}
	})
	return nil
diff --git a/commands/compose/attach.go b/commands/compose/attach.go
index 2b633dc6ebf5..6b8d72fc3954 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 e8b07ed64674..8bc0e887685c 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/send.go b/commands/compose/send.go
index c8f7cc58c80d..998975560912 100644
--- a/commands/compose/send.go
+++ b/commands/compose/send.go
@@ -12,7 +12,6 @@ import (

	"github.com/emersion/go-sasl"
	"github.com/emersion/go-smtp"
	"github.com/gdamore/tcell"
	"github.com/google/shlex"
	"github.com/miolini/datacounter"
	"github.com/pkg/errors"
@@ -225,8 +224,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
		aerc.SetStatus("Sending...")
		nbytes, err := sendAsync()
		if err != nil {
			aerc.SetStatus(" "+err.Error()).
				Color(tcell.ColorDefault, tcell.ColorRed)
			aerc.SetError(" " + err.Error())
			return
		}
		if config.CopyTo != "" {
@@ -246,8 +244,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error {
					r.Close()
					composer.Close()
				case *types.Error:
					aerc.PushStatus(" "+msg.Error.Error(), 10*time.Second).
						Color(tcell.ColorDefault, tcell.ColorRed)
					aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
					r.Close()
					composer.Close()
				}
diff --git a/commands/exec.go b/commands/exec.go
index f615b71e8388..0a5470dc30bc 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,17 +31,17 @@ func (ExecCmd) Execute(aerc *widgets.Aerc, args []string) error {
	go func() {
		err := cmd.Run()
		if err != nil {
			aerc.PushStatus(" "+err.Error(), 10*time.Second).
				Color(tcell.ColorDefault, tcell.ColorRed)
			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 966d5986e427..f9e5991ae7b1 100644
--- a/commands/msg/archive.go
+++ b/commands/msg/archive.go
@@ -7,8 +7,6 @@ import (
	"sync"
	"time"

	"github.com/gdamore/tcell"

	"git.sr.ht/~sircmpwn/aerc/commands"
	"git.sr.ht/~sircmpwn/aerc/models"
	"git.sr.ht/~sircmpwn/aerc/widgets"
@@ -87,8 +85,7 @@ func (Archive) Execute(aerc *widgets.Aerc, args []string) error {
			case *types.Done:
				wg.Done()
			case *types.Error:
				aerc.PushStatus(" "+msg.Error.Error(), 10*time.Second).
					Color(tcell.ColorDefault, tcell.ColorRed)
				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 30022f18b8a7..5a0be1ee4369 100644
--- a/commands/msg/copy.go
+++ b/commands/msg/copy.go
@@ -6,7 +6,6 @@ import (
	"time"

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

	"git.sr.ht/~sircmpwn/aerc/commands"
	"git.sr.ht/~sircmpwn/aerc/widgets"
@@ -61,8 +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.PushStatus(" "+msg.Error.Error(), 10*time.Second).
					Color(tcell.ColorDefault, tcell.ColorRed)
				aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
			}
		})
	return nil
diff --git a/commands/msg/delete.go b/commands/msg/delete.go
index fb0d1f81bdb0..9d6c7d26cfa3 100644
--- a/commands/msg/delete.go
+++ b/commands/msg/delete.go
@@ -4,8 +4,6 @@ import (
	"errors"
	"time"

	"github.com/gdamore/tcell"

	"git.sr.ht/~sircmpwn/aerc/lib"
	"git.sr.ht/~sircmpwn/aerc/models"
	"git.sr.ht/~sircmpwn/aerc/widgets"
@@ -49,8 +47,7 @@ func (Delete) Execute(aerc *widgets.Aerc, args []string) error {
		case *types.Done:
			aerc.PushStatus("Messages deleted.", 10*time.Second)
		case *types.Error:
			aerc.PushStatus(" "+msg.Error.Error(), 10*time.Second).
				Color(tcell.ColorDefault, tcell.ColorRed)
			aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
		}
	})

diff --git a/commands/msg/forward.go b/commands/msg/forward.go
index c51949ed39a6..ec603a07e3c5 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"
@@ -82,7 +83,7 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
		composer, err := widgets.NewComposer(aerc, 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 a53292e4a6c2..a6cc995d9406 100644
--- a/commands/msg/modify-labels.go
+++ b/commands/msg/modify-labels.go
@@ -7,7 +7,6 @@ import (
	"git.sr.ht/~sircmpwn/aerc/commands"
	"git.sr.ht/~sircmpwn/aerc/widgets"
	"git.sr.ht/~sircmpwn/aerc/worker/types"
	"github.com/gdamore/tcell"
)

type ModifyLabels struct{}
@@ -59,8 +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.PushStatus(" "+msg.Error.Error(), 10*time.Second).
				Color(tcell.ColorDefault, tcell.ColorRed)
			aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
		}
	})
	return nil
diff --git a/commands/msg/move.go b/commands/msg/move.go
index 37402ef1b2a5..406bf3555f2f 100644
--- a/commands/msg/move.go
+++ b/commands/msg/move.go
@@ -5,12 +5,10 @@ import (
	"strings"
	"time"

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

	"git.sr.ht/~sircmpwn/aerc/commands"
	"git.sr.ht/~sircmpwn/aerc/widgets"
	"git.sr.ht/~sircmpwn/aerc/worker/types"
	"git.sr.ht/~sircmpwn/getopt"
)

type Move struct{}
@@ -72,8 +70,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.PushStatus(" "+msg.Error.Error(), 10*time.Second).
				Color(tcell.ColorDefault, tcell.ColorRed)
			aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
		}
	})
	return nil
diff --git a/commands/msg/pipe.go b/commands/msg/pipe.go
index 9491cf5d6183..926305cf3a61 100644
--- a/commands/msg/pipe.go
+++ b/commands/msg/pipe.go
@@ -11,7 +11,6 @@ import (
	"git.sr.ht/~sircmpwn/aerc/widgets"

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

type Pipe struct{}
@@ -75,7 +74,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)
@@ -93,17 +92,17 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error {
		}()
		err = ecmd.Run()
		if err != nil {
			aerc.PushStatus(" "+err.Error(), 10*time.Second).
				Color(tcell.ColorDefault, tcell.ColorRed)
			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 ef075236fb7c..b28e769a3455 100644
--- a/commands/msg/read.go
+++ b/commands/msg/read.go
@@ -7,8 +7,6 @@ import (

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

	"github.com/gdamore/tcell"

	"git.sr.ht/~sircmpwn/aerc/lib"
	"git.sr.ht/~sircmpwn/aerc/models"
	"git.sr.ht/~sircmpwn/aerc/widgets"
@@ -95,8 +93,7 @@ func submitReadChange(aerc *widgets.Aerc, store *lib.MessageStore,
		case *types.Done:
			aerc.PushStatus(msg_success, 10*time.Second)
		case *types.Error:
			aerc.PushStatus(" "+msg.Error.Error(), 10*time.Second).
				Color(tcell.ColorDefault, tcell.ColorRed)
			aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
		}
	})
}
@@ -109,8 +106,7 @@ func submitReadChangeWg(aerc *widgets.Aerc, store *lib.MessageStore,
		case *types.Done:
			wg.Done()
		case *types.Error:
			aerc.PushStatus(" "+msg.Error.Error(), 10*time.Second).
				Color(tcell.ColorDefault, tcell.ColorRed)
			aerc.PushError(" "+msg.Error.Error(), 10*time.Second)
			*success = false
			wg.Done()
		}
diff --git a/commands/msg/reply.go b/commands/msg/reply.go
index c5ae1b60c511..473beb56d0e8 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"

@@ -127,7 +128,7 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {
		composer, err := widgets.NewComposer(aerc, 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/open.go b/commands/msgview/open.go
index 44584f98d8d3..d26688245645 100644
--- a/commands/msgview/open.go
+++ b/commands/msgview/open.go
@@ -49,20 +49,20 @@ 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
		}

		err = lib.OpenFile(tmpFile.Name())
		if err != nil {
			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 f3cbb70a5744..887b325e84a2 100644
--- a/commands/msgview/save.go
+++ b/commands/msgview/save.go
@@ -129,7 +129,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 459f405932bc..3ab1f0763985 100644
--- a/commands/term.go
+++ b/commands/term.go
@@ -6,7 +6,6 @@ import (

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

	"github.com/gdamore/tcell"
	"github.com/riywo/loginshell"
)

@@ -48,8 +47,7 @@ func TermCore(aerc *widgets.Aerc, args []string) error {
	term.OnClose = func(err error) {
		aerc.RemoveTab(term)
		if err != nil {
			aerc.PushStatus(" "+err.Error(), 10*time.Second).
				Color(tcell.ColorDefault, tcell.ColorRed)
			aerc.PushError(" "+err.Error(), 10*time.Second)
		}
	}
	return nil
diff --git a/commands/util.go b/commands/util.go
index 5529edb0d6a9..5c22d3e1e6b4 100644
--- a/commands/util.go
+++ b/commands/util.go
@@ -31,7 +31,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 {
@@ -55,7 +55,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 917dc225182e..aa4d8403881b 100644
--- a/config/aerc.conf.in
+++ b/config/aerc.conf.in
@@ -67,6 +67,17 @@ sort=
# Default: false
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 5794388a7131..2201d67f415a 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
@@ -330,6 +333,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:") {
@@ -344,6 +352,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,
@@ -404,6 +416,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
}

@@ -460,6 +485,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{},
@@ -489,6 +516,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,
@@ -499,6 +527,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
@@ -606,8 +635,17 @@ 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 = make(StyleSet)
	if err := ui.style.ParseStyleSet(ui.StyleSetName, styleSetDirs); err != nil {
		return fmt.Errorf("Error whie parsing styleset \"%s\": %s", ui.StyleSetName, err)
	}

	return nil
}

func (config AercConfig) mergeContextualUi(baseUi UIConfig,
	contextType ContextType, s string) UIConfig {
	for _, contextualUi := range config.ContextualUis {
		if contextualUi.ContextType != contextType {
			continue
@@ -617,17 +655,26 @@ 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[so]
}
diff --git a/config/default_styleset b/config/default_styleset
new file mode 100644
index 000000000000..8f84a593fd41
--- /dev/null
+++ b/config/default_styleset
@@ -0,0 +1,39 @@
# 
# 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=true

title.reverse=true
header.bold=true

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

msglist_unread.bold=true

dirlist_selecting.reverse=true
dirlist_selecting.fg=gray

completion_pill.reverse=true

tab*.default=true
tab_default.reverse=true

selecter_focused.reverse=true
selecter_chooser.bold=true

border.default = true
border.reverse = true

statusline*.default=true
statusline_default.reverse=true
statusline_error.fg=red
statusline_success.fg=green
diff --git a/config/style.go b/config/style.go
new file mode 100644
index 000000000000..2a5b97c9f28b
--- /dev/null
+++ b/config/style.go
@@ -0,0 +1,240 @@
package config

import (
	"errors"
	"os"
	"path"
	"regexp"
	"strconv"
	"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_SOFT_ERROR
	STYLE_SUCCESS

	STYLE_SELECTED
	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_SELECTED

	STYLE_DIRLIST_DEFAULT
	STYLE_DIRLIST_SELECTED
	STYLE_DIRLIST_SELECTING

	STYLE_COMPLETION_DEFAULT
	STYLE_COMPLETION_SELECTED
	STYLE_COMPLETION_GUTTER
	STYLE_COMPLETION_PILL

	STYLE_TAB_DEFAULT
	STYLE_TAB_SELECTED

	STYLE_STACK_DEFAULT

	STYLE_SELECTER_DEFAULT
	STYLE_SELECTER_FOCUSED
	STYLE_SELECTER_CHOOSER

	STYLE_SPINNER
	STYLE_BORDER
)

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

	"selected": STYLE_SELECTED,
	"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_selected": STYLE_MSGLIST_SELECTED,

	"dirlist_default":   STYLE_DIRLIST_DEFAULT,
	"dirlist_selecting": STYLE_DIRLIST_SELECTING,
	"dirlist_selected":  STYLE_DIRLIST_SELECTED,

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

	"tab_default":  STYLE_TAB_DEFAULT,
	"tab_selected": STYLE_TAB_SELECTED,

	"stack_default": STYLE_STACK_DEFAULT,

	"selecter_default": STYLE_SELECTER_DEFAULT,
	"selecter_focused": STYLE_SELECTER_FOCUSED,
	"selecter_chooser": STYLE_SELECTER_CHOOSER,

	"spinner": STYLE_SPINNER,
	"border":  STYLE_BORDER,
}

type StyleSet map[StyleObject]tcell.Style

func (ss StyleSet) reset() {
	for _, so := range StyleNames {
		ss[so] = tcell.StyleDefault
	}
}

func (ss StyleSet) updateStyle(so StyleObject, attr, val string) error {
	switch attr {
	case "fg":
		ss[so] = ss[so].Foreground(tcell.GetColor(val))
	case "bg":
		ss[so] = ss[so].Background(tcell.GetColor(val))
	case "bold":
		if state, err := strconv.ParseBool(val); err != nil {
			return errors.New("Invalid value for attribute. bold true or false")
		} else {
			ss[so] = ss[so].Bold(state)
		}
	case "blink":
		if state, err := strconv.ParseBool(val); err != nil {
			return errors.New("Invalid value for attribute. blink true or false")
		} else {
			ss[so] = ss[so].Blink(state)
		}
	case "underline":
		if state, err := strconv.ParseBool(val); err != nil {
			return errors.New("Invalid value for attribute. Underline true or false")
		} else {
			ss[so] = ss[so].Underline(state)
		}
	case "default":
		ss[so] = tcell.StyleDefault
	case "normal":
		ss[so] = ss[so].Normal()
	case "reverse":
		if state, err := strconv.ParseBool(val); err != nil {
			return errors.New(
				"Invalid value for attribute. Value:" + val + ". Reverse true or false")
		} else {
			ss[so] = ss[so].Reverse(state)
		}
	default:
		return errors.New("Unknown style attribute: " + attr)
	}

	return nil
}

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(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
	}

	ss.reset()

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

	for _, key := range defaultSection.KeyStrings() {
		index := strings.Index(key, ".")
		styleName := key[:index]
		attr := key[index+1:]
		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.updateStyle(so, attr, val); err != nil {
					return err
				}
			}
		} else {
			so, ok := StyleNames[styleName]
			if !ok {
				return errors.New("Unknown style object: " + styleName)
			}
			if err := ss.updateStyle(so, attr, val); err != nil {
				return err
			}
		}
	}

	return nil
}

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 36ac9c66f0f5..db49df514e17 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 000000000000..4fafd7b6ecd5
--- /dev/null
+++ b/doc/aerc-stylesets.7.scd
@@ -0,0 +1,181 @@
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>`

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

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

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

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

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

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

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

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

*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.
|  selected
:  The style object used for selection while not using specialized
   configuration.
|  title
:  The style object used to style titles in ui elements.
|  header
:  The style object used to style headers in ui elements.
|  error
:  The style used to show errors.
|  soft_error
:  The style used when showing soft errors.
|  success
:  The style used for success messages.
|  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_selected
:  The style used for currently selected message in a message list.
|  dirlist_default
:  The default style for directories in the directory list.
|  dirlist_selecting
:  The currently selected directory in the directory list.
|  dirlist_selected
:  The style used for the currently selected directory.
|  completion_default
:  The default style for the completion engine.
|  completion_selected
:  The currently selected item in the completion popup window.
|  completion_gutter
:  The completion gutter.
|  completion_pill
:  The completion pill.
|  tab_default
:  The default style for the tab bar.
|  tab_selected
:  The selected tab in the tab bar.
|  stack_default
:  The default style for ui stack element.
|  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.
|  spinner
:  The style for the loading spinner.
|  border
:  The style used to draw borders. *Only the background color is used*.

## 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 folling wildcards can be made using this syntax.
[[ *Example*
:[ Description
|  \*.fg=blue
:  Set the foreground color of all style objects to blue.
|  \*selected.bg=hotpink
:  Set the background color of all style objects that end in selected
   to hotpink.

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

	*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 7a757595fb75..181e9da7de66 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,8 @@ func (bordered *Bordered) Draw(ctx *Context) {
	y := 0
	width := ctx.Width()
	height := ctx.Height()
	style := tcell.StyleDefault.Reverse(true)
	// 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 690a8699eb65..603d1c267cd8 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_DEFAULT))
	}
}

diff --git a/lib/ui/tab.go b/lib/ui/tab.go
index 7d1ce63d34bc..8d543e2ffb33 100644
--- a/lib/ui/tab.go
+++ b/lib/ui/tab.go
@@ -278,9 +278,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_DEFAULT)
		if strip.Selected == i {
			style = tcell.StyleDefault
			style = strip.uiConfig.GetStyle(config.STYLE_TAB_SELECTED)
		}
		tabWidth := 32
		if ctx.Width()-x < tabWidth {
@@ -296,8 +296,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_DEFAULT))
}

func (strip *TabStrip) Invalidate() {
@@ -381,7 +381,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_DEFAULT))
	}

	tab := content.Tabs[content.Selected]
diff --git a/lib/ui/text.go b/lib/ui/text.go
index 2b82598511e2..455c2eb63ee3 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 f7301fb36cde..1ae39c347870 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)
	sel := c.uiConfig.GetStyle(config.STYLE_COMPLETION_SELECTED)
	gutter := c.uiConfig.GetStyle(config.STYLE_COMPLETION_GUTTER)
	pill := c.uiConfig.GetStyle(config.STYLE_COMPLETION_SELECTED)

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

diff --git a/widgets/account-wizard.go b/widgets/account-wizard.go
index d7b46b9ea3d9..e247dd2392bc 100644
--- a/widgets/account-wizard.go
+++ b/widgets/account-wizard.go
@@ -76,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
@@ -151,33 +151,36 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
		{ui.SIZE_WEIGHT, 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, '@') {
@@ -228,16 +231,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
	}).Columns([]ui.GridSpec{
		{ui.SIZE_WEIGHT, 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)
@@ -245,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
@@ -270,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)
@@ -305,16 +313,19 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
	}).Columns([]ui.GridSpec{
		{ui.SIZE_WEIGHT, 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)
@@ -322,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
@@ -347,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":
@@ -381,15 +394,16 @@ func NewAccountWizard(conf *config.AercConfig, aerc *Aerc) *AccountWizard {
		{ui.SIZE_WEIGHT, 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")
@@ -415,8 +429,7 @@ func (wizard *AccountWizard) ConfigureTemporaryAccount(temporary bool) {

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

	"github.com/gdamore/tcell"

@@ -54,8 +55,7 @@ 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 &AccountView{
			acct:   acct,
			aerc:   aerc,
@@ -67,7 +67,7 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon

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

	msglist := NewMessageList(conf, logger, aerc)
@@ -276,8 +276,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
		acct.labels = msg.Labels
	case *types.Error:
		acct.logger.Printf("%v", msg.Error)
		acct.host.SetStatus(fmt.Sprintf("%v", msg.Error)).
			Color(tcell.ColorDefault, tcell.ColorRed)
		acct.host.SetError(fmt.Sprintf("%v", msg.Error))
	}
}

@@ -287,7 +286,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 4c8d09df60a5..d8a04154b9c0 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -45,8 +45,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{
@@ -70,7 +70,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,
	}

@@ -374,12 +374,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) {
@@ -408,13 +416,11 @@ func (aerc *Aerc) BeginExCommand(cmd string) {
	exline := NewExLine(aerc.conf, cmd, func(cmd string) {
		parts, err := shlex.Split(cmd)
		if err != nil {
			aerc.PushStatus(" "+err.Error(), 10*time.Second).
				Color(tcell.ColorDefault, tcell.ColorRed)
			aerc.PushError(" "+err.Error(), 10*time.Second)
		}
		err = aerc.cmd(parts)
		if err != nil {
			aerc.PushStatus(" "+err.Error(), 10*time.Second).
				Color(tcell.ColorDefault, tcell.ColorRed)
			aerc.PushError(" "+err.Error(), 10*time.Second)
		}
		// only add to history if this is an unsimulated command,
		// ie one not executed from a keybinding
@@ -438,8 +444,7 @@ func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) {
		}
		err := aerc.cmd(cmd)
		if err != nil {
			aerc.PushStatus(" "+err.Error(), 10*time.Second).
				Color(tcell.ColorDefault, tcell.ColorRed)
			aerc.PushError(" "+err.Error(), 10*time.Second)
		}
	}, func(cmd string) []string {
		return nil // TODO: completions
@@ -506,7 +511,7 @@ func (aerc *Aerc) CloseBackends() error {
}

func (aerc *Aerc) GetPassword(title string, prompt string, cb func(string)) {
	aerc.getpasswd = NewGetPasswd(title, prompt, func(pw string) {
	aerc.getpasswd = NewGetPasswd(title, prompt, aerc.conf, func(pw string) {
		aerc.getpasswd = nil
		aerc.Invalidate()
		cb(pw)
diff --git a/widgets/compose.go b/widgets/compose.go
index a97e5fedc927..d420d2936bb8 100644
--- a/widgets/compose.go
+++ b/widgets/compose.go
@@ -71,10 +71,11 @@ func NewComposer(aerc *Aerc, 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 {
@@ -110,21 +111,21 @@ func NewComposer(aerc *Aerc, 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 {
@@ -141,9 +142,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)
@@ -297,7 +298,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
@@ -738,7 +741,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)
	}
@@ -792,23 +795,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))
}

@@ -869,21 +876,25 @@ func newReviewMessage(composer *Composer, err error) *reviewMessage {
		{ui.SIZE_WEIGHT, 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/[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 600b38c053c0..3fc6b574b40e 100644
--- a/widgets/dirlist.go
+++ b/widgets/dirlist.go
@@ -194,7 +194,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)
@@ -202,7 +203,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
	}
@@ -212,12 +213,11 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
		if row >= ctx.Height() {
			break
		}
		style := tcell.StyleDefault
		style := dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_DEFAULT)
		if name == dirlist.selected {
			style = style.Reverse(true)
			style = dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_SELECTED)
		} else if name == dirlist.selecting {
			style = style.Reverse(true)
			style = style.Foreground(tcell.ColorGray)
			style = dirlist.UiConfig().GetStyle(config.STYLE_DIRLIST_SELECTING)
		}
		ctx.Fill(0, row, ctx.Width(), 1, ' ', style)

diff --git a/widgets/exline.go b/widgets/exline.go
index 6def938eba3c..692c8e213666 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 08702c582d04..70fc9a97b421 100644
--- a/widgets/getpasswd.go
+++ b/widgets/getpasswd.go
@@ -3,6 +3,7 @@ package widgets
import (
	"github.com/gdamore/tcell"

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

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

func NewGetPasswd(title string, prompt string, cb func(string)) *GetPasswd {
func NewGetPasswd(title string, prompt string, conf *config.AercConfig, cb func(string)) *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()
@@ -29,10 +32,13 @@ func NewGetPasswd(title string, prompt string, cb func(string)) *GetPasswd {
}

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 f36901f6b761..96430c3d305c 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -49,7 +49,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 {
@@ -84,15 +85,17 @@ 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,
		})

		style := uiConfig.GetStyle(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)
			style = uiConfig.GetStyle(config.STYLE_MSGLIST_DELETED)
		}
		// unread message
		seen := false
@@ -102,16 +105,20 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
			}
		}
		if !seen {
			style = style.Bold(true)
			style = uiConfig.GetStyle(config.STYLE_MSGLIST_UNREAD)
		}

		ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
		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,
		})
		// marked message
		if store.IsMarked(msg.Uid) {
			style = uiConfig.GetStyle(config.STYLE_MSGLIST_MARKED)
		}

		// current row
		if row == ml.store.SelectedIndex()-ml.scroll {
			style = uiConfig.GetStyle(config.STYLE_MSGLIST_SELECTED)
		}

		ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
		fmtStr, args, err := format.ParseMessageFormat(
			ml.aerc.SelectedAccount().acct.From,
			uiConfig.IndexFormat,
@@ -284,7 +291,8 @@ func (ml *MessageList) Scroll() {
}

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 35fc4b6e3bae..4d300de75204 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)
@@ -345,7 +351,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.GetStyle(config.STYLE_SELECTED)
		}
		ctx.Fill(0, y+i, ctx.Width(), 1, ' ', style)
		name := fmt.Sprintf("%s/%s",
			strings.ToLower(part.part.MIMEType),
@@ -434,6 +443,7 @@ func (mv *MessageViewer) Focus(focus bool) {

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

func NewPartViewer(acct *AccountView, conf *config.AercConfig,
@@ -517,7 +528,8 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
		{ui.SIZE_WEIGHT, 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":
@@ -530,6 +542,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
	grid.AddChild(selecter).At(2, 0)

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

	if term != nil {
@@ -638,14 +652,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 dc03cf63f800..e41731c3a464 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)
	softErrorStyle := p.uiConfig.GetStyle(config.STYLE_SOFT_ERROR)
	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(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 {
@@ -47,14 +47,15 @@ func (p *PGPInfo) DrawSignature(ctx *ui.Context) {
			break
		}
		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
	var ident *openpgp.Identity
	// TODO: Pick identity more intelligently
@@ -63,12 +64,13 @@ func (p *PGPInfo) DrawEncryption(ctx *ui.Context, y int) {
	}

	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 7fae9cda453d..0faf37eafa15 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_SELECTER_DEFAULT))
	x := 2
	for i, option := range sel.options {
		style := tcell.StyleDefault
		style := sel.uiConfig.GetStyle(config.STYLE_SELECTER_DEFAULT)
		if sel.focus == i {
			if sel.focused {
				style = style.Reverse(true)
				style = sel.uiConfig.GetStyle(config.STYLE_SELECTER_FOCUSED)
			} else if sel.chooser {
				style = style.Bold(true)
				style = sel.uiConfig.GetStyle(config.STYLE_SELECTER_CHOOSER)
			}
		}
		x += ctx.Printf(x, 1, style, "[%s]", option)
diff --git a/widgets/spinner.go b/widgets/spinner.go
index 51b8c1b09a55..0c7242219f38 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 8d0a1aec3750..d6d7761b0f01 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)
@@ -84,6 +90,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
}
@@ -92,7 +110,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 0ac67e5b6997..1322a0aaa7cd 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.26.0
Details
Message ID
<20200412173900.6mfgbrnsbqkh4ogd@feather.localdomain>
In-Reply-To
<20200410195640.1005042-1-sri@vathsan.com> (view parent)
DKIM signature
fail
Download raw message
DKIM signature: fail
Hi Srivathsan,

On Fri, Apr 10, 2020 at 09:56:40PM +0200, Srivathsan Murali wrote:
> Alright, Sorry for the spam today. Found more time than I thought I
> would. So lots of changes to this patch.

Spam? Please. Polishing a patch is by no means a bad thing...
We can certainly live with a mail more in the inbox :)

Some comments regarding the functionality:

# improve option stacking
	The message_* options stack strangely
	Say you have a config like
	```
	*.default=true
	*selected.reverse=true

	msglist_unread.bold=true
	msglist_unread.fg=yellow
	```
	(incomplete, but you get the idea)
	If the unread message isn't selected, it works as advertised
	and you end up with (bold, yellow fg, default bg)
	However, if the message is selected what I expect to happen is
	"take the msglist_unread settings and reverse it"
	so in essence (bold, yellow bg, default fg)
	However what does happen is that the default bg / default fg are reversed
	and you end up with (default fg reversed / default bg reversed / non bold)
	Do you think we can address that?
	The stacking should be "msglist_default" --> "msglist_unread" --> "msglist_flagged"
	in my view.
	("see next point")

# add msglist_flagged
	It would be cool if we could add a style for "flagged" messages.

# dirlist_selecting
	We don't really need that. We can just use dirlist_selected for both.
	As we do not support "highlighting" and "opening" in a separate step this is
	transient. Plus it is rather confusing what the difference is in my opinion.

# soft_error
	This I found hard to understand. How about we go with "warning" instead?
	That seems to be a more common phrase.
	But as far as I can see we only use this in the pgp code?
	Do you think it would be better as a specific option like valid_PGP is?

I'll look into the code in a bit.
The man page rewrite seems good to me. At least I could understand it pretty well
I think.

Thanks a lot for all the work that you put into this, I really appreciate it.

Greetings,
Reto
Details
Message ID
<C2PI83SVW5R1.3MGYIOP3TLFAF@enceladus>
In-Reply-To
<20200412173900.6mfgbrnsbqkh4ogd@feather.localdomain> (view parent)
DKIM signature
pass
Download raw message
Hi Reto,

Well its been a while. Thanks for sending the new rebased version.
Here is a late response to your comments on the last patchset.

I hope to make some time in the next few days to work further on
this patchset.

On Sun Apr 12, 2020 at 9:39 PM CEST, Reto wrote:
> # improve option stacking
> The message_* options stack strangely
> Say you have a config like
> ```
> *.default=true
> *selected.reverse=true
>
> msglist_unread.bold=true
> msglist_unread.fg=yellow
> ```
> (incomplete, but you get the idea)
> If the unread message isn't selected, it works as advertised
> and you end up with (bold, yellow fg, default bg)
> However, if the message is selected what I expect to happen is
> "take the msglist_unread settings and reverse it"
> so in essence (bold, yellow bg, default fg)
> However what does happen is that the default bg / default fg are
> reversed
> and you end up with (default fg reversed / default bg reversed / non
> bold)
> Do you think we can address that?
> The stacking should be "msglist_default" --> "msglist_unread" -->
> "msglist_flagged"
> in my view.
> ("see next point")
As implemented this is expected behaviour because currently there is
no stacking of styles built into the parsing or retrieval.
Each individual StyleObject is given a style and doesn't really
relate to another one.

When the style for STYLE_MSGLIST_SELECTED is retrieved, the flags
on a message are not used to get a style more in line with what you
are envisioning.

I can see the value in such a stacking system and would require treating
certain objects such as selected differently. They could be modifiers
to a StyleObject instead of being one themselves.

I will work on this a bit before sending a new patchset.

> # add msglist_flagged
> It would be cool if we could add a style for "flagged" messages.
Not an issue, will add this for the next version of the patchset.

> # dirlist_selecting
> We don't really need that. We can just use dirlist_selected for both.
> As we do not support "highlighting" and "opening" in a separate step
> this is
> transient. Plus it is rather confusing what the difference is in my
> opinion.
Will fix this for the next version of the patchset.

> # soft_error
> This I found hard to understand. How about we go with "warning" instead?
> That seems to be a more common phrase.
> But as far as I can see we only use this in the pgp code?
> Do you think it would be better as a specific option like valid_PGP is?
Warning sounds better to me too.

--
Cheers,
Srivathsan Murali (sri)
Review patch Export thread (mbox)