about summary refs log tree commit diff stats
path: root/widgets
diff options
context:
space:
mode:
authorKalyan Sriram <coder.kalyan@gmail.com>2020-07-27 01:03:55 -0700
committerReto Brunner <reto@labrat.space>2020-07-30 19:35:59 +0200
commit1ff687ca2b0821c2cacc1fa725abb3302d2af9da (patch)
treeb84df04a645c1fd2ee94d7a08f2f9c717930e9ab /widgets
parent1bab1754f095a5c0537fc639d0214f6efbb340a2 (diff)
downloadaerc-1ff687ca2b0821c2cacc1fa725abb3302d2af9da.tar.gz
Implement style configuration.
Introduce the ability to configure stylesets, allowing customization of
aerc's look (color scheme, font weight, etc). Default styleset is
installed to /path/to/aerc/stylesets/default.
Diffstat (limited to 'widgets')
-rw-r--r--widgets/account-wizard.go102
-rw-r--r--widgets/account.go5
-rw-r--r--widgets/aerc.go20
-rw-r--r--widgets/compose.go68
-rw-r--r--widgets/dirlist.go10
-rw-r--r--widgets/exline.go6
-rw-r--r--widgets/getpasswd.go19
-rw-r--r--widgets/msglist.go50
-rw-r--r--widgets/msgviewer.go63
-rw-r--r--widgets/pgpinfo.go34
-rw-r--r--widgets/selector.go (renamed from widgets/selecter.go)46
-rw-r--r--widgets/spinner.go6
-rw-r--r--widgets/status.go49
-rw-r--r--widgets/tabhost.go3
14 files changed, 288 insertions, 193 deletions
diff --git a/widgets/account-wizard.go b/widgets/account-wizard.go
index 4e51926..ae45bb8 100644
--- a/widgets/account-wizard.go
+++ b/widgets/account-wizard.go
@@ -75,21 +75,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 +150,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 +230,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 +250,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 +277,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 +312,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 +332,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 +359,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 +393,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")
diff --git a/widgets/account.go b/widgets/account.go
index 211f09d..53c65ba 100644
--- a/widgets/account.go
+++ b/widgets/account.go
@@ -64,15 +64,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)
diff --git a/widgets/aerc.go b/widgets/aerc.go
index 4913be3..692e00d 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) *StatusMessage {
+	return aerc.statusline.PushError(text)
+}
+
+func (aerc *Aerc) PushSuccess(text string) *StatusMessage {
+	return aerc.statusline.PushSuccess(text)
 }
 
 func (aerc *Aerc) focus(item ui.Interactive) {
@@ -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..03c9175 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))
 		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,8 @@ 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))
 		return nil
 	}
 	return signature
@@ -648,7 +650,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 +706,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 +790,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..3cdc5cf 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,17 @@ 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 +35,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..e38dd9e 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -50,7 +50,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 +102,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,
@@ -342,7 +355,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 107ff59..30c83f7 100644
--- a/widgets/msgviewer.go
+++ b/widgets/msgviewer.go
@@ -33,6 +33,7 @@ type MessageViewer struct {
 	grid     *ui.Grid
 	switcher *PartSwitcher
 	msg      lib.MessageView
+	uiConfig config.UIConfig
 }
 
 type PartSwitcher struct {
@@ -62,9 +63,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(),
 			}
 		},
 	)
@@ -94,15 +97,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 {
@@ -116,6 +120,7 @@ func NewMessageViewer(acct *AccountView,
 		grid:     grid,
 		msg:      msg,
 		switcher: switcher,
+		uiConfig: acct.UiConfig(),
 	}
 	switcher.mv = mv
 
@@ -224,8 +229,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)
@@ -347,7 +353,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),
@@ -436,6 +445,7 @@ func (mv *MessageViewer) Focus(focus bool) {
 
 type PartViewer struct {
 	ui.Invalidatable
+	conf        *config.AercConfig
 	err         error
 	fetched     bool
 	filter      *exec.Cmd
@@ -450,6 +460,7 @@ type PartViewer struct {
 	term        *Terminal
 	selecter    *Selecter
 	grid        *ui.Grid
+	uiConfig    config.UIConfig
 }
 
 func NewPartViewer(acct *AccountView, conf *config.AercConfig,
@@ -519,7 +530,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":
@@ -532,6 +544,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
 	grid.AddChild(selecter).At(2, 0)
 
 	pv := &PartViewer{
+		conf:        conf,
 		filter:      filter,
 		index:       index,
 		msg:         msg,
@@ -543,6 +556,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
 		term:        term,
 		selecter:    selecter,
 		grid:        grid,
+		uiConfig:    acct.UiConfig(),
 	}
 
 	if term != nil {
@@ -661,14 +675,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)
@@ -679,8 +695,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)
@@ -702,8 +718,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) {
@@ -711,18 +729,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/selector.go
index 7fae9cd..d19d38f 100644
--- a/widgets/selecter.go
+++ b/widgets/selector.go
@@ -3,46 +3,50 @@ package widgets
 import (
 	"github.com/gdamore/tcell"
 
+	"git.sr.ht/~sircmpwn/aerc/config"
 	"git.sr.ht/~sircmpwn/aerc/lib/ui"
 )
 
-type Selecter struct {
+type Selector 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 {
-	return &Selecter{
-		focus:   focus,
-		options: options,
+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) {
-	ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
+func (sel *Selector) Draw(ctx *ui.Context) {
+	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)
@@ -50,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/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..122ca5f 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) *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) *StatusMessage {
+	msg := status.Push(text, 10*time.Second)
+	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..28c9be0 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) *StatusMessage
+	PushSuccess(text string) *StatusMessage
 	Beep()
 }