summaryrefslogtreecommitdiff
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
parent1bab1754f095a5c0537fc639d0214f6efbb340a2 (diff)
downloadaerc-1ff687ca2b0821c2cacc1fa725abb3302d2af9da.zip
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.
-rw-r--r--Makefile7
-rw-r--r--commands/compose/attach.go5
-rw-r--r--commands/compose/detach.go5
-rw-r--r--commands/exec.go15
-rw-r--r--commands/msg/pipe.go14
-rw-r--r--config/aerc.conf.in11
-rw-r--r--config/config.go55
-rw-r--r--config/default_styleset33
-rw-r--r--config/style.go379
-rw-r--r--doc/aerc-config.5.scd14
-rw-r--r--doc/aerc-stylesets.7.scd189
-rw-r--r--lib/ui/borders.go13
-rw-r--r--lib/ui/stack.go10
-rw-r--r--lib/ui/tab.go11
-rw-r--r--lib/ui/text.go42
-rw-r--r--lib/ui/textinput.go32
-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
30 files changed, 1032 insertions, 284 deletions
diff --git a/Makefile b/Makefile
index 1e7fbd6..1c1de75 100644
--- a/Makefile
+++ b/Makefile
@@ -35,7 +35,8 @@ DOCS := \
aerc-notmuch.5 \
aerc-smtp.5 \
aerc-tutorial.7 \
- aerc-templates.7
+ aerc-templates.7 \
+ aerc-stylesets.7
.1.scd.1:
scdoc < $< > $@
@@ -58,7 +59,7 @@ clean:
install: all
mkdir -m755 -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 $(DESTDIR)$(MANDIR)/man5 $(DESTDIR)$(MANDIR)/man7 \
- $(DESTDIR)$(SHAREDIR) $(DESTDIR)$(SHAREDIR)/filters $(DESTDIR)$(SHAREDIR)/templates
+ $(DESTDIR)$(SHAREDIR) $(DESTDIR)$(SHAREDIR)/filters $(DESTDIR)$(SHAREDIR)/templates $(DESTDIR)$(SHAREDIR)/stylesets
install -m755 aerc $(DESTDIR)$(BINDIR)/aerc
install -m644 aerc.1 $(DESTDIR)$(MANDIR)/man1/aerc.1
install -m644 aerc-search.1 $(DESTDIR)$(MANDIR)/man1/aerc-search.1
@@ -70,6 +71,7 @@ install: all
install -m644 aerc-smtp.5 $(DESTDIR)$(MANDIR)/man5/aerc-smtp.5
install -m644 aerc-tutorial.7 $(DESTDIR)$(MANDIR)/man7/aerc-tutorial.7
install -m644 aerc-templates.7 $(DESTDIR)$(MANDIR)/man7/aerc-templates.7
+ install -m644 aerc-stylesets.7 $(DESTDIR)$(MANDIR)/man7/aerc-stylesets.7
install -m644 config/accounts.conf $(DESTDIR)$(SHAREDIR)/accounts.conf
install -m644 aerc.conf $(DESTDIR)$(SHAREDIR)/aerc.conf
install -m644 config/binds.conf $(DESTDIR)$(SHAREDIR)/binds.conf
@@ -78,6 +80,7 @@ install: all
install -m755 filters/plaintext $(DESTDIR)$(SHAREDIR)/filters/plaintext
install -m644 templates/quoted_reply $(DESTDIR)$(SHAREDIR)/templates/quoted_reply
install -m644 templates/forward_as_body $(DESTDIR)$(SHAREDIR)/templates/forward_as_body
+ install -m644 config/default_styleset $(DESTDIR)$(SHAREDIR)/stylesets/default
RMDIR_IF_EMPTY:=sh -c '\
if test -d $$0 && ! ls -1qA $$0 | grep -q . ; then \
diff --git a/commands/compose/attach.go b/commands/compose/attach.go
index 2b633dc..148442b 100644
--- a/commands/compose/attach.go
+++ b/commands/compose/attach.go
@@ -4,11 +4,9 @@ import (
"fmt"
"os"
"strings"
- "time"
"git.sr.ht/~sircmpwn/aerc/commands"
"git.sr.ht/~sircmpwn/aerc/widgets"
- "github.com/gdamore/tcell"
"github.com/mitchellh/go-homedir"
)
@@ -52,8 +50,7 @@ func (Attach) Execute(aerc *widgets.Aerc, args []string) error {
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()))
return nil
}
diff --git a/commands/compose/detach.go b/commands/compose/detach.go
index e8b07ed..b48159d 100644
--- a/commands/compose/detach.go
+++ b/commands/compose/detach.go
@@ -3,10 +3,8 @@ package compose
import (
"fmt"
"strings"
- "time"
"git.sr.ht/~sircmpwn/aerc/widgets"
- "github.com/gdamore/tcell"
)
type Detach struct{}
@@ -44,8 +42,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))
return nil
}
diff --git a/commands/exec.go b/commands/exec.go
index e15afbe..7d24fdc 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{}
@@ -35,14 +33,15 @@ func (ExecCmd) Execute(aerc *widgets.Aerc, args []string) error {
if err != nil {
aerc.PushError(" " + err.Error())
} 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()))
+ } 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/pipe.go b/commands/msg/pipe.go
index 20cb8b4..4e4ba67 100644
--- a/commands/msg/pipe.go
+++ b/commands/msg/pipe.go
@@ -12,7 +12,6 @@ import (
"git.sr.ht/~sircmpwn/aerc/worker/types"
"git.sr.ht/~sircmpwn/getopt"
- "github.com/gdamore/tcell"
)
type Pipe struct{}
@@ -96,14 +95,15 @@ func (Pipe) Execute(aerc *widgets.Aerc, args []string) error {
if err != nil {
aerc.PushError(" " + err.Error())
} 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()))
+ } 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/config/aerc.conf.in b/config/aerc.conf.in
index 3348efa..b9381a8 100644
--- a/config/aerc.conf.in
+++ b/config/aerc.conf.in
@@ -67,6 +67,17 @@ sort=
# Default: true
next-message-on-delete=true
+# The directories where the stylesets are stored. It takes a colon-separated
+# list of directories.
+#
+# default: @SHAREDIR@/stylesets/
+stylesets-dirs=@SHAREDIR@/stylesets/
+
+# Sets the styleset to use for the aerc ui elements.
+#
+# Default: default
+styleset-name=default
+
[viewer]
#
# Specifies the pager to use when displaying emails. Note that some filters
diff --git a/config/config.go b/config/config.go
index 00a52ce..9e78c86 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
@@ -411,6 +414,19 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
}
}
}
+
+ if err := config.Ui.loadStyleSet(
+ config.Ui.StyleSetDirs); err != nil {
+ return err
+ }
+
+ for idx, _ := range config.ContextualUis {
+ if err := config.ContextualUis[idx].UiConfig.loadStyleSet(
+ config.Ui.StyleSetDirs); err != nil {
+ return err
+ }
+ }
+
return nil
}
@@ -471,6 +487,8 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
NextMessageOnDelete: true,
CompletionDelay: 250 * time.Millisecond,
CompletionPopovers: true,
+ StyleSetDirs: []string{path.Join(sharedir, "stylesets")},
+ StyleSetName: "default",
},
ContextualUis: []UIConfigContext{},
@@ -500,6 +518,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
Forwards: "forward_as_body",
},
}
+
// These bindings are not configurable
config.Bindings.AccountWizard.ExKey = KeyStroke{
Key: tcell.KeyCtrlE,
@@ -510,6 +529,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
if err = config.LoadConfig(file); err != nil {
return nil, err
}
+
if ui, err := file.GetSection("general"); err == nil {
if err := ui.MapTo(&config.General); err != nil {
return nil, err
@@ -617,8 +637,18 @@ func parseLayout(layout string) [][]string {
return l
}
-func (config *AercConfig) mergeContextualUi(baseUi *UIConfig,
- contextType ContextType, s string) {
+func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error {
+ ui.style = NewStyleSet()
+ err := ui.style.LoadStyleSet(ui.StyleSetName, styleSetDirs)
+ if err != nil {
+ return fmt.Errorf("Unable to load default styleset: %s", err)
+ }
+
+ return nil
+}
+
+func (config AercConfig) mergeContextualUi(baseUi UIConfig,
+ contextType ContextType, s string) UIConfig {
for _, contextualUi := range config.ContextualUis {
if contextualUi.ContextType != contextType {
continue
@@ -628,17 +658,30 @@ func (config *AercConfig) mergeContextualUi(baseUi *UIConfig,
continue
}
- mergo.MergeWithOverwrite(baseUi, contextualUi.UiConfig)
- return
+ mergo.Merge(&baseUi, contextualUi.UiConfig, mergo.WithOverride)
+ if contextualUi.UiConfig.StyleSetName != "" {
+ baseUi.style = contextualUi.UiConfig.style
+ }
+ return baseUi
}
+
+ return baseUi
}
-func (config *AercConfig) GetUiConfig(params map[ContextType]string) UIConfig {
+func (config AercConfig) GetUiConfig(params map[ContextType]string) UIConfig {
baseUi := config.Ui
for k, v := range params {
- config.mergeContextualUi(&baseUi, k, v)
+ baseUi = config.mergeContextualUi(baseUi, k, v)
}
return baseUi
}
+
+func (uiConfig UIConfig) GetStyle(so StyleObject) tcell.Style {
+ return uiConfig.style.Get(so)
+}
+
+func (uiConfig UIConfig) GetStyleSelected(so StyleObject) tcell.Style {
+ return uiConfig.style.Selected(so)
+}
diff --git a/config/default_styleset b/config/default_styleset
new file mode 100644
index 0000000..fa52f23
--- /dev/null
+++ b/config/default_styleset
@@ -0,0 +1,33 @@
+#
+# aerc default styleset
+#
+# This styleset uses the terminal defaults as its base.
+# More information on how to configure the styleset can be found in
+# the *aerc-styleset.7* manpage. Please read the manual before
+# modifying or creating a styleset.
+
+*.default=true
+*.selected.reverse=toggle
+
+title.reverse=true
+header.bold=true
+
+*error.bold=true
+error.fg=red
+warning.fg=yellow
+success.fg=green
+
+statusline*.default=true
+statusline_default.reverse=true
+statusline_error.fg=red
+statusline_error.reverse=true
+
+msglist_unread.bold=true
+
+completion_pill.reverse=true
+
+tab.reverse=true
+border.reverse = true
+
+selector_focused.reverse=true
+selector_chooser.bold=true
diff --git a/config/style.go b/config/style.go
new file mode 100644
index 0000000..f159be3
--- /dev/null
+++ b/config/style.go
@@ -0,0 +1,379 @@
+package config
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path"
+ "regexp"
+ "strings"
+
+ "github.com/gdamore/tcell"
+ "github.com/go-ini/ini"
+ "github.com/mitchellh/go-homedir"
+)
+
+type StyleObject int32
+
+const (
+ STYLE_DEFAULT StyleObject = iota
+ STYLE_ERROR
+ STYLE_WARNING
+ STYLE_SUCCESS
+
+ STYLE_TITLE
+ STYLE_HEADER
+
+ STYLE_STATUSLINE_DEFAULT
+ STYLE_STATUSLINE_ERROR
+ STYLE_STATUSLINE_SUCCESS
+
+ STYLE_MSGLIST_DEFAULT
+ STYLE_MSGLIST_UNREAD
+ STYLE_MSGLIST_READ
+ STYLE_MSGLIST_DELETED
+ STYLE_MSGLIST_MARKED
+ STYLE_MSGLIST_FLAGGED
+
+ STYLE_DIRLIST_DEFAULT
+
+ STYLE_COMPLETION_DEFAULT
+ STYLE_COMPLETION_GUTTER
+ STYLE_COMPLETION_PILL
+
+ STYLE_TAB
+ STYLE_STACK
+ STYLE_SPINNER
+ STYLE_BORDER
+
+ STYLE_SELECTOR_DEFAULT
+ STYLE_SELECTOR_FOCUSED
+ STYLE_SELECTOR_CHOOSER
+)
+
+var StyleNames = map[string]StyleObject{
+ "default": STYLE_DEFAULT,
+ "error": STYLE_ERROR,
+ "warning": STYLE_WARNING,
+ "success": STYLE_SUCCESS,
+
+ "title": STYLE_TITLE,
+ "header": STYLE_HEADER,
+
+ "statusline_default": STYLE_STATUSLINE_DEFAULT,
+ "statusline_error": STYLE_STATUSLINE_ERROR,
+ "statusline_success": STYLE_STATUSLINE_SUCCESS,
+
+ "msglist_default": STYLE_MSGLIST_DEFAULT,
+ "msglist_unread": STYLE_MSGLIST_UNREAD,
+ "msglist_read": STYLE_MSGLIST_READ,
+ "msglist_deleted": STYLE_MSGLIST_DELETED,
+ "msglist_marked": STYLE_MSGLIST_MARKED,
+ "msglist_flagged": STYLE_MSGLIST_FLAGGED,
+
+ "dirlist_default": STYLE_DIRLIST_DEFAULT,
+
+ "completion_default": STYLE_COMPLETION_DEFAULT,
+ "completion_gutter": STYLE_COMPLETION_GUTTER,
+ "completion_pill": STYLE_COMPLETION_PILL,
+
+ "tab": STYLE_TAB,
+ "stack": STYLE_STACK,
+ "spinner": STYLE_SPINNER,
+ "border": STYLE_BORDER,
+
+ "selector_default": STYLE_SELECTOR_DEFAULT,
+ "selector_focused": STYLE_SELECTOR_FOCUSED,
+ "selector_chooser": STYLE_SELECTOR_CHOOSER,
+}
+
+type Style struct {
+ Fg tcell.Color
+ Bg tcell.Color
+ Bold bool
+ Blink bool
+ Underline bool
+ Reverse bool
+}
+
+func (s Style) Get() tcell.Style {
+ return tcell.StyleDefault.
+ Foreground(s.Fg).
+ Background(s.Bg).
+ Bold(s.Bold).
+ Blink(s.Blink).
+ Underline(s.Blink).
+ Reverse(s.Reverse)
+}
+
+func (s *Style) Normal() {
+ s.Bold = false
+ s.Blink = false
+ s.Underline = false
+ s.Reverse = false
+}
+
+func (s *Style) Default() *Style {
+ s.Fg = tcell.ColorDefault
+ s.Bg = tcell.ColorDefault
+ return s
+}
+
+func (s *Style) Reset() *Style {
+ s.Default()
+ s.Normal()
+ return s
+}
+
+func boolSwitch(val string, cur_val bool) (bool, error) {
+ switch val {
+ case "true":
+ return true, nil
+ case "false":
+ return false, nil
+ case "toggle":
+ return !cur_val, nil
+ default:
+ return cur_val, errors.New(
+ "Bool Switch attribute must be true, false, or toggle")
+ }
+}
+
+func (s *Style) Set(attr, val string) error {
+ switch attr {
+ case "fg":
+ s.Fg = tcell.GetColor(val)
+ case "bg":
+ s.Bg = tcell.GetColor(val)
+ case "bold":
+ if state, err := boolSwitch(val, s.Bold); err != nil {
+ return err
+ } else {
+ s.Bold = state
+ }
+ case "blink":
+ if state, err := boolSwitch(val, s.Blink); err != nil {
+ return err
+ } else {
+ s.Blink = state
+ }
+ case "underline":
+ if state, err := boolSwitch(val, s.Underline); err != nil {
+ return err
+ } else {
+ s.Underline = state
+ }
+ case "reverse":
+ if state, err := boolSwitch(val, s.Reverse); err != nil {
+ return err
+ } else {
+ s.Reverse = state
+ }
+ case "default":
+ s.Default()
+ case "normal":
+ s.Normal()
+ default:
+ return errors.New("Unknown style attribute: " + attr)
+ }
+
+ return nil
+}
+
+type StyleSet struct {
+ objects map[StyleObject]*Style
+ selected map[StyleObject]*Style
+}
+
+func NewStyleSet() StyleSet {
+ ss := StyleSet{
+ objects: make(map[StyleObject]*Style),
+ selected: make(map[StyleObject]*Style),
+ }
+ for _, so := range StyleNames {
+ ss.objects[so] = new(Style)
+ ss.selected[so] = new(Style)
+ }
+
+ return ss
+}
+
+func (ss StyleSet) reset() {
+ for _, so := range StyleNames {
+ ss.objects[so].Reset()
+ ss.selected[so].Reset()
+ }
+}
+
+func (ss StyleSet) Get(so StyleObject) tcell.Style {
+ return ss.objects[so].Get()
+}
+
+func (ss StyleSet) Selected(so StyleObject) tcell.Style {
+ return ss.selected[so].Get()
+}
+
+func findStyleSet(stylesetName string, stylesetsDir []string) (string, error) {
+ for _, dir := range stylesetsDir {
+ stylesetPath, err := homedir.Expand(path.Join(dir, stylesetName))
+ if err != nil {
+ return "", err
+ }
+
+ if _, err := os.Stat(stylesetPath); os.IsNotExist(err) {
+ continue
+ }
+
+ return stylesetPath, nil
+ }
+
+ return "", fmt.Errorf(
+ "Can't find styleset %q in any of %v", stylesetName, stylesetsDir)
+}
+
+func (ss *StyleSet) ParseStyleSet(file *ini.File) error {
+ ss.reset()
+
+ defaultSection, err := file.GetSection(ini.DefaultSection)
+ if err != nil {
+ return err
+ }
+
+ selectedKeys := []string{}
+
+ for _, key := range defaultSection.KeyStrings() {
+ tokens := strings.Split(key, ".")
+ var styleName, attr string
+ switch len(tokens) {
+ case 2:
+ styleName, attr = tokens[0], tokens[1]
+ case 3:
+ if tokens[1] != "selected" {
+ return errors.New("Unknown modifier: " + tokens[1])
+ }
+ selectedKeys = append(selectedKeys, key)
+ continue
+ default:
+ return errors.New("Style parsing error: " + key)
+ }
+ val := defaultSection.KeysHash()[key]
+
+ if strings.ContainsAny(styleName, "*?") {
+ regex := fnmatchToRegex(styleName)
+ for sn, so := range StyleNames {
+ matched, err := regexp.MatchString(regex, sn)
+ if err != nil {
+ return err
+ }
+
+ if !matched {
+ continue
+ }
+
+ if err := ss.objects[so].Set(attr, val); err != nil {
+ return err
+ }
+ if err := ss.selected[so].Set(attr, val); err != nil {
+ return err
+ }
+ }
+ } else {
+ so, ok := StyleNames[styleName]
+ if !ok {
+ return errors.New("Unknown style object: " + styleName)
+ }
+ if err := ss.objects[so].Set(attr, val); err != nil {
+ return err
+ }
+ if err := ss.selected[so].Set(attr, val); err != nil {
+ return err
+ }
+ }
+ }
+
+ for _, key := range selectedKeys {
+ tokens := strings.Split(key, ".")
+ styleName, modifier, attr := tokens[0], tokens[1], tokens[2]
+ if modifier != "selected" {
+ return errors.New("Unknown modifier: " + modifier)
+ }
+
+ val := defaultSection.KeysHash()[key]
+
+ if strings.ContainsAny(styleName, "*?") {
+ regex := fnmatchToRegex(styleName)
+ for sn, so := range StyleNames {
+ matched, err := regexp.MatchString(regex, sn)
+ if err != nil {
+ return err
+ }
+
+ if !matched {
+ continue
+ }
+
+ if err := ss.selected[so].Set(attr, val); err != nil {
+ return err
+ }
+ }
+ } else {
+ so, ok := StyleNames[styleName]
+ if !ok {
+ return errors.New("Unknown style object: " + styleName)
+ }
+ if err := ss.selected[so].Set(attr, val); err != nil {
+ return err
+ }
+ }
+ }
+
+ for _, key := range defaultSection.KeyStrings() {
+ tokens := strings.Split(key, ".")
+ styleName, attr := tokens[0], tokens[1]
+ val := defaultSection.KeysHash()[key]
+
+ if styleName != "selected" {
+ continue
+ }
+
+ for _, so := range StyleNames {
+ if err := ss.selected[so].Set(attr, val); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+func (ss *StyleSet) LoadStyleSet(stylesetName string, stylesetDirs []string) error {
+ filepath, err := findStyleSet(stylesetName, stylesetDirs)
+ if err != nil {
+ return err
+ }
+
+ file, err := ini.Load(filepath)
+ if err != nil {
+ return err
+ }
+
+ return ss.ParseStyleSet(file)
+}
+
+func fnmatchToRegex(pattern string) string {
+ n := len(pattern)
+ var regex strings.Builder
+
+ for i := 0; i < n; i++ {
+ switch pattern[i] {
+ case '*':
+ regex.WriteString(".*")
+ case '?':
+ regex.WriteByte('.')
+ default:
+ regex.WriteByte(pattern[i])
+ }
+ }
+
+ return regex.String()
+}
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index af64ad6..fcd70ec 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -173,6 +173,20 @@ These options are configured in the *[ui]* section of aerc.conf.
Default: 250ms
+*stylesets-dirs*
+ The directories where the stylesets are stored. The config takes a
+ colon-seperated list of dirs.
+
+ Default: "/usr/share/aerc/stylesets"
+
+*styleset-name*
+ The name of the styleset to be used to style the ui elements. The
+ stylesets are stored in the 'stylesets' directory in the config
+ directory.
+
+ Default: default
+
+
## Contextual UI Configuration
The UI configuration can be specialized for accounts, specific mail
diff --git a/doc/aerc-stylesets.7.scd b/doc/aerc-stylesets.7.scd
new file mode 100644
index 0000000..829418e
--- /dev/null
+++ b/doc/aerc-stylesets.7.scd
@@ -0,0 +1,189 @@
+aerc-stylesets(7)
+
+# Name
+
+aerc-stylesets - styleset file specification for *aerc*(1)
+
+# SYNOPSIS
+
+aerc uses a simple configuration syntax to configure the styleset for
+its ui.
+
+# Styleset Configuration
+
+Aerc uses a simple configuration file to describe a styleset. The
+styleset is described as key, value pairs. In each line, the key
+represents the style object it signifies and the color/atrribute of
+that is modified.
+
+For example, in the line below, the foreground color of the
+style object "msglist_unread" is set to "cornflowerblue"
+```
+msglist_unread.fg=cornflowerblue
+```
+
+The configuration also allows wildcard matching of the style_objects
+to configure multiple style objects at a time.
+
+## Style
+The following options are available to be modified for each of the
+style objects.
+
+*fg*
+ The foreground color of the style object is set.
+
+ Syntax: `<style_object>.fg=<color>`
+
+*bg*
+ The background color of the style object is set.
+
+ Syntax: `<style_object>.bg=<color>`
+
+*bold*
+ The bold attribute of the style object is set/unset.
+
+ Syntax: `<style_object>.bold=<true|false|toggle>`
+
+*blink*
+ The blink attribute of the style object is set/unset.
+ _The terminal needs to support blinking text_
+
+ Syntax: `<style_object>.bold=<true|false|toggle>`
+
+*underline*
+ The underline attribute of the style object is set/unset.
+ _The terminal needs to support underline text_
+
+ Syntax: `<style_object>.underline=<true|false|toggle>`
+
+*reverse*
+ Reverses the color of the style object. Exchanges the foreground
+ and background colors.
+
+ Syntax: `<style_object>.reverse=<true|false|toggle>`
+ _If the value is false, it doesn't change anything_
+
+*normal*
+ All the attributes of the style object are unset.
+
+ Syntax: `<style_object>.normal=<true>`
+ _The value doesn't matter_
+
+*default*
+ Set the style object to the default style of the context. Usually
+ based on the terminal.
+
+ Syntax: `<style_object>.default=<true>`
+ _The value doesn't matter_
+
+## Style Objects
+The style objects represent the various ui elements or ui instances for
+styling.
+
+[[ *Style Object*
+:[ *Description*
+| default
+: The default style object used for normal ui elements while not
+ using specialized configuration.
+| error
+: The style used to show errors.
+| warning
+: The style used when showing warnings.
+| success
+: The style used for success messages.
+| title
+: The style object used to style titles in ui elements.
+| header
+: The style object used to style headers in ui elements.
+| statusline_default
+: The default style applied to the statusline.
+| statusline_error
+: The style used for error messages in statusline.
+| statusline_success
+: The style used for success messages in statusline.
+| msglist_default
+: The default style for messages in a message list.
+| msglist_unread
+: Unread messages in a message list.
+| msglist_read
+: Read messages in a message list.
+| msglist_deleted
+: The messages marked as deleted.
+| msglist_marked
+: The messages with the marked flag.
+| msglist_flagged
+: The messages with the flagged flag.
+| dirlist_default
+: The default style for directories in the directory list.
+| completion_default
+: The default style for the completion engine.
+| completion_gutter
+: The completion gutter.
+| completion_pill
+: The completion pill.
+| tab
+: The style for the tab bar.
+| stack
+: The style for ui stack element.
+| spinner
+: The style for the loading spinner.
+| border
+: The style used to draw borders. *Only the background color is used*.
+| selecter_default
+: The default style for the selecter ui element.
+| selecter_focused
+: The focused item in a selecter ui element.
+| selecter_chooser
+: The item chooser in a selecter ui element.
+
+## fnmatch style wildcard matching
+The styleset configuration can be made simpler by using the fnmatch
+style wildcard matching for the style object.
+
+The special characters used in the fnmatch wildcards are:
+[[ *Pattern*
+:[ *Meaning*
+| \*
+: Matches everything
+| \?
+: Matches any single character
+
+For example, the following wildcards can be made using this syntax.
+[[ *Example*
+:[ Description
+| \*.fg=blue
+: Set the foreground color of all style objects to blue.
+| \*list.bg=hotpink
+: Set the background color of all style objects that end in list
+ to hotpink.
+
+## Selected modifier
+Selected modifier can be applied to any style object. The style provided for
+the selected modifier are applied on top of the style object it corresponds to.
+
+If you would like to make sure message that are flagged as read in the msglist
+appear in yellow foreground and black background. You can specify that with
+this.
+
+\tmsglist_default.selected.fg=yellow
+\tmsglist_default.selected.bg=black
+
+If we specify the global style selected modifer using fnmatch as below:
+
+\t\*.selected.reverse=toggle
+
+This toggles the reverse switch for selected version of all the style objects.
+
+## Colors
+The color values are set using the values accepted by the tcell library.
+The values can be one of the following.
+
+ *default*
+ The color is set as per the system or terminal default.
+
+ *<Color name>*
+ Any w3c approved color name is used to set colors for the style.
+
+ *<Hex code>*
+ Hexcode for a color can be used. The format must be "\#XXXXXX"
+
diff --git a/lib/ui/borders.go b/lib/ui/borders.go
index 7a75759..99d6880 100644
--- a/lib/ui/borders.go
+++ b/lib/ui/borders.go
@@ -2,6 +2,8 @@ package ui
import (
"github.com/gdamore/tcell"
+
+ "git.sr.ht/~sircmpwn/aerc/config"
)
const (
@@ -16,12 +18,15 @@ type Bordered struct {
borders uint
content Drawable
onInvalidate func(d Drawable)
+ uiConfig config.UIConfig
}
-func NewBordered(content Drawable, borders uint) *Bordered {
+func NewBordered(
+ content Drawable, borders uint, uiConfig config.UIConfig) *Bordered {
b := &Bordered{
- borders: borders,
- content: content,
+ borders: borders,
+ content: content,
+ uiConfig: uiConfig,
}
content.OnInvalidate(b.contentInvalidated)
return b
@@ -44,7 +49,7 @@ func (bordered *Bordered) Draw(ctx *Context) {
y := 0
width := ctx.Width()
height := ctx.Height()
- style := tcell.StyleDefault.Reverse(true)
+ style := bordered.uiConfig.GetStyle(config.STYLE_BORDER)
if bordered.borders&BORDER_LEFT != 0 {
ctx.Fill(0, 0, 1, ctx.Height(), ' ', style)
x += 1
diff --git a/lib/ui/stack.go b/lib/ui/stack.go
index 690a869..c9004a0 100644
--- a/lib/ui/stack.go
+++ b/lib/ui/stack.go
@@ -3,16 +3,19 @@ package ui
import (
"fmt"
+ "git.sr.ht/~sircmpwn/aerc/config"
+
"github.com/gdamore/tcell"
)
type Stack struct {
children []Drawable
onInvalidate []func(d Drawable)
+ uiConfig config.UIConfig
}
-func NewStack() *Stack {
- return &Stack{}
+func NewStack(uiConfig config.UIConfig) *Stack {
+ return &Stack{uiConfig: uiConfig}
}
func (stack *Stack) Children() []Drawable {
@@ -33,7 +36,8 @@ func (stack *Stack) Draw(ctx *Context) {
if len(stack.children) > 0 {
stack.Peek().Draw(ctx)
} else {
- ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
+ ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
+ stack.uiConfig.GetStyle(config.STYLE_STACK))
}
}
diff --git a/lib/ui/tab.go b/lib/ui/tab.go
index 4b99e4b..cd5f448 100644
--- a/lib/ui/tab.go
+++ b/lib/ui/tab.go
@@ -283,9 +283,9 @@ func (tabs *Tabs) removeHistory(index int) {
func (strip *TabStrip) Draw(ctx *Context) {
x := 0
for i, tab := range strip.Tabs {
- style := tcell.StyleDefault.Reverse(true)
+ style := strip.uiConfig.GetStyle(config.STYLE_TAB)
if strip.Selected == i {
- style = tcell.StyleDefault
+ style = strip.uiConfig.GetStyleSelected(config.STYLE_TAB)
}
tabWidth := 32
if ctx.Width()-x < tabWidth {
@@ -301,8 +301,8 @@ func (strip *TabStrip) Draw(ctx *Context) {
break
}
}
- style := tcell.StyleDefault.Reverse(true)
- ctx.Fill(x, 0, ctx.Width()-x, 1, ' ', style)
+ ctx.Fill(x, 0, ctx.Width()-x, 1, ' ',
+ strip.uiConfig.GetStyle(config.STYLE_TAB))
}
func (strip *TabStrip) Invalidate() {
@@ -386,7 +386,8 @@ func (content *TabContent) Draw(ctx *Context) {
if content.Selected >= len(content.Tabs) {
width := ctx.Width()
height := ctx.Height()
- ctx.Fill(0, 0, width, height, ' ', tcell.StyleDefault)
+ ctx.Fill(0, 0, width, height, ' ',
+ content.uiConfig.GetStyle(config.STYLE_TAB))
}
tab := content.Tabs[content.Selected]
diff --git a/lib/ui/text.go b/lib/ui/text.go
index 2b82598..455c2eb 100644
--- a/lib/ui/text.go
+++ b/lib/ui/text.go
@@ -15,17 +15,13 @@ type Text struct {
Invalidatable
text string
strategy uint
- fg tcell.Color
- bg tcell.Color
- bold bool
- reverse bool
+ style tcell.Style
}
-func NewText(text string) *Text {
+func NewText(text string, style tcell.Style) *Text {
return &Text{
- bg: tcell.ColorDefault,
- fg: tcell.ColorDefault,
- text: text,
+ text: text,
+ style: style,
}
}
@@ -41,25 +37,6 @@ func (t *Text) Strategy(strategy uint) *Text {
return t
}
-func (t *Text) Bold(bold bool) *Text {
- t.bold = bold
- t.Invalidate()
- return t
-}
-
-func (t *Text) Color(fg tcell.Color, bg tcell.Color) *Text {
- t.fg = fg
- t.bg = bg
- t.Invalidate()
- return t
-}
-
-func (t *Text) Reverse(reverse bool) *Text {
- t.reverse = reverse
- t.Invalidate()
- return t
-}
-
func (t *Text) Draw(ctx *Context) {
size := runewidth.StringWidth(t.text)
x := 0
@@ -69,15 +46,8 @@ func (t *Text) Draw(ctx *Context) {
if t.strategy == TEXT_RIGHT {
x = ctx.Width() - size
}
- style := tcell.StyleDefault.Background(t.bg).Foreground(t.fg)
- if t.bold {
- style = style.Bold(true)
- }
- if t.reverse {
- style = style.Reverse(true)
- }
- ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', style)
- ctx.Printf(x, 0, style, "%s", t.text)
+ ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', t.style)
+ ctx.Printf(x, 0, t.style, "%s", t.text)
}
func (t *Text) Invalidate() {
diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go
index f6b0c72..2445065 100644
--- a/lib/ui/textinput.go
+++ b/lib/ui/textinput.go
@@ -6,6 +6,8 @@ import (
"github.com/gdamore/tcell"
"github.com/mattn/go-runewidth"
+
+ "git.sr.ht/~sircmpwn/aerc/config"
)
// TODO: Attach history providers
@@ -27,16 +29,18 @@ type TextInput struct {
completeIndex int
completeDelay time.Duration
completeDebouncer *time.Timer
+ uiConfig config.UIConfig
}
// Creates a new TextInput. TextInputs will render a "textbox" in the entire
// context they're given, and process keypresses to build a string from user
// input.
-func NewTextInput(text string) *TextInput {
+func NewTextInput(text string, ui config.UIConfig) *TextInput {
return &TextInput{
- cells: -1,
- text: []rune(text),
- index: len([]rune(text)),
+ cells: -1,
+ text: []rune(text),
+ index: len([]rune(text)),
+ uiConfig: ui,
}
}
@@ -87,16 +91,18 @@ func (ti *TextInput) Draw(ctx *Context) {
ti.ensureScroll()
}
ti.ctx = ctx // gross
- ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
+
+ defaultStyle := ti.uiConfig.GetStyle(config.STYLE_DEFAULT)
+ ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', defaultStyle)
text := ti.text[scroll:]
sindex := ti.index - scroll
if ti.password {
- x := ctx.Printf(0, 0, tcell.StyleDefault, "%s", ti.prompt)
+ x := ctx.Printf(0, 0, defaultStyle, "%s", ti.prompt)
cells := runewidth.StringWidth(string(text))
- ctx.Fill(x, 0, cells, 1, '*', tcell.StyleDefault)
+ ctx.Fill(x, 0, cells, 1, '*', defaultStyle)
} else {
- ctx.Printf(0, 0, tcell.StyleDefault, "%s%s", ti.prompt, string(text))
+ ctx.Printf(0, 0, defaultStyle, "%s%s", ti.prompt, string(text))
}
cells := runewidth.StringWidth(string(text[:sindex]) + ti.prompt)
if ti.focus {
@@ -126,6 +132,7 @@ func (ti *TextInput) drawPopover(ctx *Context) {
ti.Set(stem + ti.StringRight())
ti.Invalidate()
},
+ uiConfig: ti.uiConfig,
}
width := maxLen(ti.completions) + 3
height := len(ti.completions)
@@ -353,6 +360,7 @@ type completions struct {
onSelect func(int)
onExec func()
onStem func(string)
+ uiConfig config.UIConfig
}
func maxLen(ss []string) int {
@@ -367,10 +375,10 @@ func maxLen(ss []string) int {
}
func (c *completions) Draw(ctx *Context) {
- bg := tcell.StyleDefault
- sel := tcell.StyleDefault.Reverse(true)
- gutter := tcell.StyleDefault
- pill := tcell.StyleDefault.Reverse(true)
+ bg := c.uiConfig.GetStyle(config.STYLE_COMPLETION_DEFAULT)
+ gutter := c.uiConfig.GetStyle(config.STYLE_COMPLETION_GUTTER)
+ pill := c.uiConfig.GetStyle(config.STYLE_COMPLETION_PILL)
+ sel := c.uiConfig.GetStyleSelected(config.STYLE_COMPLETION_DEFAULT)
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', bg)
diff --git a/widgets/account-wizard.go b/widgets/account-wizard.go
index 4e51926..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()
}