diff options
author | Reto Brunner <reto@labrat.space> | 2020-05-27 07:37:02 +0200 |
---|---|---|
committer | Reto Brunner <reto@labrat.space> | 2020-05-27 07:57:10 +0200 |
commit | 0f78f06610c0e8887aba2ae50e99b86477a384b3 (patch) | |
tree | ff4cd6581d3af0911954a37550602366d2bb0e2f /config | |
parent | 6c4ed3cfe2fe66a1e5f26c404ea90e048142db72 (diff) | |
download | aerc-0f78f06610c0e8887aba2ae50e99b86477a384b3.tar.gz |
Add Style configuration
The following functionalities are added to configure aerc ui styles.
- Read stylesets from file with very basic fnmatch wildcard matching
- Add default styleset
- Support different stylesets as part of UiConfig allowing contextual
styles.
- Move widgets/ui elements to use the stylesets.
- Add configuration manual for the styleset
Diffstat (limited to 'config')
-rw-r--r-- | config/aerc.conf.in | 11 | ||||
-rw-r--r-- | config/config.go | 63 | ||||
-rw-r--r-- | config/default_styleset | 33 | ||||
-rw-r--r-- | config/style.go | 372 |
4 files changed, 473 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 8ebd69d3..c4307241 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 @@ -332,6 +335,11 @@ func (config *AercConfig) LoadConfig(file *ini.File) error { if err := ui.MapTo(&config.Ui); err != nil { return err } + + stylesetsDirs := ui.Key("stylesets-dirs").String() + if stylesetsDirs != "" { + config.Ui.StyleSetDirs = strings.Split(stylesetsDirs, ":") + } } for _, sectionName := range file.SectionStrings() { if !strings.Contains(sectionName, "ui:") { @@ -346,6 +354,10 @@ func (config *AercConfig) LoadConfig(file *ini.File) error { if err := uiSection.MapTo(&uiSubConfig); err != nil { return err } + stylesetsDirs := uiSection.Key("stylesets-dirs").String() + if stylesetsDirs != "" { + uiSubConfig.StyleSetDirs = strings.Split(stylesetsDirs, ":") + } contextualUi := UIConfigContext{ UiConfig: uiSubConfig, @@ -406,6 +418,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 } @@ -466,6 +491,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{}, @@ -495,6 +522,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, @@ -505,6 +533,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 @@ -612,8 +641,17 @@ func parseLayout(layout string) [][]string { return l } -func (config *AercConfig) mergeContextualUi(baseUi *UIConfig, - contextType ContextType, s string) { +func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error { + ui.style = NewStyleSet() + if err := ui.style.ParseStyleSet(ui.StyleSetName, styleSetDirs); err != nil { + return fmt.Errorf("Error whie parsing styleset \"%s\": %s", ui.StyleSetName, err) + } + + return nil +} + +func (config AercConfig) mergeContextualUi(baseUi UIConfig, + contextType ContextType, s string) UIConfig { for _, contextualUi := range config.ContextualUis { if contextualUi.ContextType != contextType { continue @@ -623,17 +661,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..9e918aeb --- /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.fg=red +warning.fg=yellow +*error.bold=true +success.fg=green + +statusline*.default=true +statusline_default.reverse=true +statusline_error.fg=red +statusline_success.fg=green + +msglist_unread.bold=true + +completion_pill.reverse=true + +tab.reverse=true +border.reverse = true + +selecter_focused.reverse=true +selecter_chooser.bold=true diff --git a/config/style.go b/config/style.go new file mode 100644 index 00000000..fb17d93f --- /dev/null +++ b/config/style.go @@ -0,0 +1,372 @@ +package config + +import ( + "errors" + "os" + "path" + "regexp" + "strings" + + "github.com/gdamore/tcell" + "github.com/go-ini/ini" + "github.com/mitchellh/go-homedir" +) + +type StyleObject int32 + +const ( + STYLE_DEFAULT StyleObject = iota + STYLE_ERROR + STYLE_WARNING + STYLE_SUCCESS + + STYLE_TITLE + STYLE_HEADER + + STYLE_STATUSLINE_DEFAULT + STYLE_STATUSLINE_ERROR + STYLE_STATUSLINE_SUCCESS + + STYLE_MSGLIST_DEFAULT + STYLE_MSGLIST_UNREAD + STYLE_MSGLIST_READ + STYLE_MSGLIST_DELETED + STYLE_MSGLIST_MARKED + STYLE_MSGLIST_FLAGGED + + STYLE_DIRLIST_DEFAULT + + STYLE_COMPLETION_DEFAULT + STYLE_COMPLETION_GUTTER + STYLE_COMPLETION_PILL + + STYLE_TAB + STYLE_STACK + STYLE_SPINNER + STYLE_BORDER + + STYLE_SELECTER_DEFAULT + STYLE_SELECTER_FOCUSED + STYLE_SELECTER_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, + + "selecter_default": STYLE_SELECTER_DEFAULT, + "selecter_focused": STYLE_SELECTER_FOCUSED, + "selecter_chooser": STYLE_SELECTER_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 "", errors.New("Can't find styleset - " + stylesetName) +} +func (ss *StyleSet) ParseStyleSet(stylesetName string, stylesetDirs []string) error { + filepath, err := findStyleSet(stylesetName, stylesetDirs) + if err != nil { + return err + } + + file, err := ini.Load(filepath) + if err != nil { + return err + } + + ss.reset() + + defaultSection, err := file.GetSection(ini.DefaultSection) + if err != nil { + return err + } + + 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 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() +} |