diff options
author | Kalyan Sriram <coder.kalyan@gmail.com> | 2020-07-27 01:03:55 -0700 |
---|---|---|
committer | Reto Brunner <reto@labrat.space> | 2020-07-30 19:35:59 +0200 |
commit | 1ff687ca2b0821c2cacc1fa725abb3302d2af9da (patch) | |
tree | b84df04a645c1fd2ee94d7a08f2f9c717930e9ab /config/style.go | |
parent | 1bab1754f095a5c0537fc639d0214f6efbb340a2 (diff) | |
download | aerc-1ff687ca2b0821c2cacc1fa725abb3302d2af9da.tar.gz |
Implement style configuration.
Introduce the ability to configure stylesets, allowing customization of aerc's look (color scheme, font weight, etc). Default styleset is installed to /path/to/aerc/stylesets/default.
Diffstat (limited to 'config/style.go')
-rw-r--r-- | config/style.go | 379 |
1 files changed, 379 insertions, 0 deletions
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() +} |