aboutsummaryrefslogtreecommitdiffstats
path: root/config
diff options
context:
space:
mode:
authorKalyan Sriram <coder.kalyan@gmail.com>2020-07-27 01:03:55 -0700
committerReto Brunner <reto@labrat.space>2020-08-06 21:42:06 +0200
commit905cb9dfd3ef197bb4b59039a1be76ce2c8e3099 (patch)
tree2d923c42ec224b1d525d942a7bb17416f4a62dd5 /config
parent548a5fff68a648a5e0b6fd909e3c21463addc8af (diff)
downloadaerc-905cb9dfd3ef197bb4b59039a1be76ce2c8e3099.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 'config')
-rw-r--r--config/aerc.conf.in11
-rw-r--r--config/config.go66
-rw-r--r--config/default_styleset33
-rw-r--r--config/style.go379
4 files changed, 483 insertions, 6 deletions
diff --git a/config/aerc.conf.in b/config/aerc.conf.in
index 3348efac..b9381a8b 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 00a52ce7..12096baf 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,30 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
}
}
}
+
+ if err := config.Ui.loadStyleSet(
+ config.Ui.StyleSetDirs); err != nil {
+ return err
+ }
+
+ for idx, contextualUi := range config.ContextualUis {
+ if contextualUi.UiConfig.StyleSetName == "" &&
+ len(contextualUi.UiConfig.StyleSetDirs) == 0 {
+ continue // no need to do anything if nothing is overridden
+ }
+ // fill in the missing part from the base
+ if contextualUi.UiConfig.StyleSetName == "" {
+ config.ContextualUis[idx].UiConfig.StyleSetName = config.Ui.StyleSetName
+ } else if len(contextualUi.UiConfig.StyleSetDirs) == 0 {
+ config.ContextualUis[idx].UiConfig.StyleSetDirs = config.Ui.StyleSetDirs
+ }
+ // since at least one of them has changed, load the styleset
+ if err := config.ContextualUis[idx].UiConfig.loadStyleSet(
+ config.ContextualUis[idx].UiConfig.StyleSetDirs); err != nil {
+ return err
+ }
+ }
+
return nil
}
@@ -471,6 +498,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 +529,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 +540,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 +648,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 +669,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 00000000..fa52f232
--- /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 00000000..f159be3d
--- /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()
+}