package config import ( "errors" "fmt" "os" "regexp" "strconv" "strings" "git.sr.ht/~rjarry/aerc/lib/xdg" "git.sr.ht/~rockorager/vaxis" "github.com/emersion/go-message/mail" "github.com/go-ini/ini" ) 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_WARNING STYLE_STATUSLINE_SUCCESS STYLE_MSGLIST_DEFAULT STYLE_MSGLIST_UNREAD STYLE_MSGLIST_READ STYLE_MSGLIST_FLAGGED STYLE_MSGLIST_DELETED STYLE_MSGLIST_MARKED STYLE_MSGLIST_RESULT STYLE_MSGLIST_ANSWERED STYLE_MSGLIST_THREAD_FOLDED STYLE_MSGLIST_GUTTER STYLE_MSGLIST_PILL STYLE_MSGLIST_THREAD_CONTEXT STYLE_MSGLIST_THREAD_ORPHAN STYLE_DIRLIST_DEFAULT STYLE_DIRLIST_UNREAD STYLE_DIRLIST_RECENT STYLE_PART_SWITCHER STYLE_PART_FILENAME STYLE_PART_MIMETYPE 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_warning": STYLE_STATUSLINE_WARNING, "statusline_success": STYLE_STATUSLINE_SUCCESS, "msglist_default": STYLE_MSGLIST_DEFAULT, "msglist_unread": STYLE_MSGLIST_UNREAD, "msglist_read": STYLE_MSGLIST_READ, "msglist_flagged": STYLE_MSGLIST_FLAGGED, "msglist_deleted": STYLE_MSGLIST_DELETED, "msglist_marked": STYLE_MSGLIST_MARKED, "msglist_result": STYLE_MSGLIST_RESULT, "msglist_answered": STYLE_MSGLIST_ANSWERED, "msglist_gutter": STYLE_MSGLIST_GUTTER, "msglist_pill": STYLE_MSGLIST_PILL, "msglist_thread_folded": STYLE_MSGLIST_THREAD_FOLDED, "msglist_thread_context": STYLE_MSGLIST_THREAD_CONTEXT, "msglist_thread_orphan": STYLE_MSGLIST_THREAD_ORPHAN, "dirlist_default": STYLE_DIRLIST_DEFAULT, "dirlist_unread": STYLE_DIRLIST_UNREAD, "dirlist_recent": STYLE_DIRLIST_RECENT, "part_switcher": STYLE_PART_SWITCHER, "part_filename": STYLE_PART_FILENAME, "part_mimetype": STYLE_PART_MIMETYPE, "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 vaxis.Color Bg vaxis.Color Bold bool Blink bool Underline bool Reverse bool Italic bool Dim bool header string // only for msglist pattern string // only for msglist re *regexp.Regexp // only for msglist } func (s Style) Get() vaxis.Style { vx := vaxis.Style{ Foreground: s.Fg, Background: s.Bg, } if s.Bold { vx.Attribute |= vaxis.AttrBold } if s.Blink { vx.Attribute |= vaxis.AttrBlink } if s.Underline { vx.UnderlineStyle |= vaxis.UnderlineSingle } if s.Reverse { vx.Attribute |= vaxis.AttrReverse } if s.Italic { vx.Attribute |= vaxis.AttrItalic } if s.Dim { vx.Attribute |= vaxis.AttrDim } return vx } func (s *Style) Normal() { s.Bold = false s.Blink = false s.Underline = false s.Reverse = false s.Italic = false s.Dim = false } func (s *Style) Default() *Style { s.Fg = 0 s.Bg = 0 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 extractColor(val string) vaxis.Color { // Check if the string can be interpreted as a number, indicating a // reference to the color number. Otherwise retrieve the number based // on the name. if i, err := strconv.ParseUint(val, 10, 8); err == nil { return vaxis.IndexColor(uint8(i)) } if strings.HasPrefix(val, "#") { val = strings.TrimPrefix(val, "#") hex, err := strconv.ParseUint(val, 16, 32) if err != nil { return 0 } return vaxis.HexColor(uint32(hex)) } return colorNames[val] } func (s *Style) Set(attr, val string) error { switch attr { case "fg": s.Fg = extractColor(val) case "bg": s.Bg = extractColor(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 "italic": if state, err := boolSwitch(val, s.Italic); err != nil { return err } else { s.Italic = state } case "dim": if state, err := boolSwitch(val, s.Dim); err != nil { return err } else { s.Dim = state } case "default": s.Default() case "normal": s.Normal() default: return errors.New("Unknown style attribute: " + attr) } return nil } func (s Style) composeWith(styles []*Style) Style { newStyle := s for _, st := range styles { if st.Fg != s.Fg && st.Fg != 0 { newStyle.Fg = st.Fg } if st.Bg != s.Bg && st.Bg != 0 { newStyle.Bg = st.Bg } if st.Bold != s.Bold { newStyle.Bold = st.Bold } if st.Blink != s.Blink { newStyle.Blink = st.Blink } if st.Underline != s.Underline { newStyle.Underline = st.Underline } if st.Reverse != s.Reverse { newStyle.Reverse = st.Reverse } if st.Italic != s.Italic { newStyle.Italic = st.Italic } if st.Dim != s.Dim { newStyle.Dim = st.Dim } } return newStyle } type StyleConf struct { base Style dynamic []Style } type StyleSet struct { objects map[StyleObject]*StyleConf selected map[StyleObject]*StyleConf user map[string]*Style path string } const defaultStyleset string = ` *.selected.bg = 12 *.selected.fg = 15 *.selected.bold = true statusline_*.dim = true statusline_*.bg = 8 statusline_*.fg = 15 *warning.fg = 3 *success.fg = 2 *error.fg = 1 *error.bold = true border.fg = 12 border.bold = true title.bg = 12 title.fg = 15 title.bold = true header.fg = 4 header.bold = true msglist_unread.bold = true msglist_deleted.dim = true msglist_marked.bg = 6 msglist_marked.fg = 15 msglist_pill.bg = 12 msglist_pill.fg = 15 part_mimetype.fg = 12 selector_chooser.bold = true selector_focused.bold = true selector_focused.bg = 12 selector_focused.fg = 15 completion_pill.bg = 12 completion_default.bg = 8 completion_default.fg = 15 ` func NewStyleSet() StyleSet { ss := StyleSet{ objects: make(map[StyleObject]*StyleConf), selected: make(map[StyleObject]*StyleConf), user: make(map[string]*Style), } for _, so := range StyleNames { ss.objects[so] = new(StyleConf) ss.selected[so] = new(StyleConf) } f, err := ini.Load([]byte(defaultStyleset)) if err == nil { err = ss.ParseStyleSet(f) } if err != nil { panic(err) } return ss } func (c *StyleConf) getStyle(h *mail.Header) *Style { if h == nil { return &c.base } for _, s := range c.dynamic { val, _ := h.Text(s.header) if s.re.MatchString(val) { s = c.base.composeWith([]*Style{&s}) return &s } } return &c.base } func (ss StyleSet) Get(so StyleObject, h *mail.Header) vaxis.Style { return ss.objects[so].getStyle(h).Get() } func (ss StyleSet) Selected(so StyleObject, h *mail.Header) vaxis.Style { return ss.selected[so].getStyle(h).Get() } func (ss StyleSet) UserStyle(name string) vaxis.Style { if style, found := ss.user[name]; found { return style.Get() } return vaxis.Style{} } func (ss StyleSet) Compose( so StyleObject, sos []StyleObject, h *mail.Header, ) vaxis.Style { base := *ss.objects[so].getStyle(h) styles := make([]*Style, len(sos)) for i, so := range sos { styles[i] = ss.objects[so].getStyle(h) } return base.composeWith(styles).Get() } func (ss StyleSet) ComposeSelected( so StyleObject, sos []StyleObject, h *mail.Header, ) vaxis.Style { base := *ss.selected[so].getStyle(h) styles := make([]*Style, len(sos)) for i, so := range sos { styles[i] = ss.selected[so].getStyle(h) } return base.composeWith(styles).Get() } func findStyleSet(stylesetName string, stylesetsDir []string) (string, error) { for _, dir := range stylesetsDir { stylesetPath := xdg.ExpandHome(dir, stylesetName) 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 { defaultSection, err := file.GetSection(ini.DefaultSection) if err != nil { return err } // parse non-selected items first for _, key := range defaultSection.Keys() { err = ss.parseKey(key, false) if err != nil { return err } } // override with selected items afterwards for _, key := range defaultSection.Keys() { err = ss.parseKey(key, true) if err != nil { return err } } user, err := file.GetSection("user") if err != nil { // This errors if the section doesn't exist, which is ok return nil } for _, key := range user.KeyStrings() { tokens := strings.Split(key, ".") var styleName, attr string switch len(tokens) { case 2: styleName, attr = tokens[0], tokens[1] default: return errors.New("Style parsing error: " + key) } val := user.KeysHash()[key] s, ok := ss.user[styleName] if !ok { // Haven't seen this name before, add it to the map s = &Style{} ss.user[styleName] = s } if err := s.Set(attr, val); err != nil { return fmt.Errorf("[user].%s=%s: %w", key, val, err) } } return nil } var styleObjRe = regexp.MustCompile(`^([\w\*\?]+)(?:\.([\w-]+),(.+?))?(\.selected)?\.(\w+)$`) func (ss *StyleSet) parseKey(key *ini.Key, selected bool) error { groups := styleObjRe.FindStringSubmatch(key.Name()) if groups == nil { return errors.New("invalid style syntax: " + key.Name()) } if (groups[4] == ".selected") != selected { return nil } obj, attr := groups[1], groups[5] header, pattern := groups[2], groups[3] objRe, err := fnmatchToRegex(obj) if err != nil { return err } num := 0 for sn, so := range StyleNames { if !objRe.MatchString(sn) { continue } if !selected { err = ss.objects[so].update(header, pattern, attr, key.Value()) if err != nil { return fmt.Errorf("%s=%s: %w", key.Name(), key.Value(), err) } } err = ss.selected[so].update(header, pattern, attr, key.Value()) if err != nil { return fmt.Errorf("%s=%s: %w", key.Name(), key.Value(), err) } num++ } if num == 0 { return errors.New("unknown style object: " + obj) } return nil } func (c *StyleConf) update(header, pattern, attr, val string) error { if header == "" || pattern == "" { return (&c.base).Set(attr, val) } for i := range c.dynamic { s := &c.dynamic[i] if s.header == header && s.pattern == pattern { return s.Set(attr, val) } } s := Style{ header: header, pattern: pattern, } if strings.HasPrefix(pattern, "~") { pattern = pattern[1:] } else { pattern = "^" + regexp.QuoteMeta(pattern) + "$" } re, err := regexp.Compile(pattern) if err != nil { return err } err = (&s).Set(attr, val) if err != nil { return err } s.re = re c.dynamic = append(c.dynamic, s) return nil } func (ss *StyleSet) LoadStyleSet(stylesetName string, stylesetDirs []string) error { filepath, err := findStyleSet(stylesetName, stylesetDirs) if err != nil { return err } var options ini.LoadOptions options.SpaceBeforeInlineComment = true file, err := ini.LoadSources(options, filepath) if err != nil { return err } ss.path = filepath return ss.ParseStyleSet(file) } func fnmatchToRegex(pattern string) (*regexp.Regexp, error) { p := regexp.QuoteMeta(pattern) p = strings.ReplaceAll(p, `\*`, `.*`) return regexp.Compile(strings.ReplaceAll(p, `\?`, `.`)) } var colorNames = map[string]vaxis.Color{ "black": vaxis.IndexColor(0), "maroon": vaxis.IndexColor(1), "green": vaxis.IndexColor(2), "olive": vaxis.IndexColor(3), "navy": vaxis.IndexColor(4), "purple": vaxis.IndexColor(5), "teal": vaxis.IndexColor(6), "silver": vaxis.IndexColor(7), "gray": vaxis.IndexColor(8), "red": vaxis.IndexColor(9), "lime": vaxis.IndexColor(10), "yellow": vaxis.IndexColor(11), "blue": vaxis.IndexColor(12), "fuchsia": vaxis.IndexColor(13), "aqua": vaxis.IndexColor(14), "white": vaxis.IndexColor(15), "aliceblue": vaxis.HexColor(0xF0F8FF), "antiquewhite": vaxis.HexColor(0xFAEBD7), "aquamarine": vaxis.HexColor(0x7FFFD4), "azure": vaxis.HexColor(0xF0FFFF), "beige": vaxis.HexColor(0xF5F5DC), "bisque": vaxis.HexColor(0xFFE4C4), "blanchedalmond": vaxis.HexColor(0xFFEBCD), "blueviolet": vaxis.HexColor(0x8A2BE2), "brown": vaxis.HexColor(0xA52A2A), "burlywood": vaxis.HexColor(0xDEB887), "cadetblue": vaxis.HexColor(0x5F9EA0), "chartreuse": vaxis.HexColor(0x7FFF00), "chocolate": vaxis.HexColor(0xD2691E), "coral": vaxis.HexColor(0xFF7F50), "cornflowerblue": vaxis.HexColor(0x6495ED), "cornsilk": vaxis.HexColor(0xFFF8DC), "crimson": vaxis.HexColor(0xDC143C), "darkblue": vaxis.HexColor(0x00008B), "darkcyan": vaxis.HexColor(0x008B8B), "darkgoldenrod": vaxis.HexColor(0xB8860B), "darkgray": vaxis.HexColor(0xA9A9A9), "darkgreen": vaxis.HexColor(0x006400), "darkkhaki": vaxis.HexColor(0xBDB76B), "darkmagenta": vaxis.HexColor(0x8B008B), "darkolivegreen": vaxis.HexColor(0x556B2F), "darkorange": vaxis.HexColor(0xFF8C00), "darkorchid": vaxis.HexColor(0x9932CC), "darkred": vaxis.HexColor(0x8B0000), "darksalmon": vaxis.HexColor(0xE9967A), "darkseagreen": vaxis.HexColor(0x8FBC8F), "darkslateblue": vaxis.HexColor(0x483D8B), "darkslategray": vaxis.HexColor(0x2F4F4F), "darkturquoise": vaxis.HexColor(0x00CED1), "darkviolet": vaxis.HexColor(0x9400D3), "deeppink": vaxis.HexColor(0xFF1493), "deepskyblue": vaxis.HexColor(0x00BFFF), "dimgray": vaxis.HexColor(0x696969), "dodgerblue": vaxis.HexColor(0x1E90FF), "firebrick": vaxis.HexColor(0xB22222), "floralwhite": vaxis.HexColor(0xFFFAF0), "forestgreen": vaxis.HexColor(0x228B22), "gainsboro": vaxis.HexColor(0xDCDCDC), "ghostwhite": vaxis.HexColor(0xF8F8FF), "gold": vaxis.HexColor(0xFFD700), "goldenrod": vaxis.HexColor(0xDAA520), "greenyellow": vaxis.HexColor(0xADFF2F), "honeydew": vaxis.HexColor(0xF0FFF0), "hotpink": vaxis.HexColor(0xFF69B4), "indianred": vaxis.HexColor(0xCD5C5C), "indigo": vaxis.HexColor(0x4B0082), "ivory": vaxis.HexColor(0xFFFFF0), "khaki": vaxis.HexColor(0xF0E68C), "lavender": vaxis.HexColor(0xE6E6FA), "lavenderblush": vaxis.HexColor(0xFFF0F5), "lawngreen": vaxis.HexColor(0x7CFC00), "lemonchiffon": vaxis.HexColor(0xFFFACD), "lightblue": vaxis.HexColor(0xADD8E6), "lightcoral": vaxis.HexColor(0xF08080), "lightcyan": vaxis.HexColor(0xE0FFFF), "lightgoldenrodyellow": vaxis.HexColor(0xFAFAD2), "lightgray": vaxis.HexColor(0xD3D3D3), "lightgreen": vaxis.HexColor(0x90EE90), "lightpink": vaxis.HexColor(0xFFB6C1), "lightsalmon": vaxis.HexColor(0xFFA07A), "lightseagreen": vaxis.HexColor(0x20B2AA), "lightskyblue": vaxis.HexColor(0x87CEFA), "lightslategray": vaxis.HexColor(0x778899), "lightsteelblue": vaxis.HexColor(0xB0C4DE), "lightyellow": vaxis.HexColor(0xFFFFE0), "limegreen": vaxis.HexColor(0x32CD32), "linen": vaxis.HexColor(0xFAF0E6), "mediumaquamarine": vaxis.HexColor(0x66CDAA), "mediumblue": vaxis.HexColor(0x0000CD), "mediumorchid": vaxis.HexColor(0xBA55D3), "mediumpurple": vaxis.HexColor(0x9370DB), "mediumseagreen": vaxis.HexColor(0x3CB371), "mediumslateblue": vaxis.HexColor(0x7B68EE), "mediumspringgreen": vaxis.HexColor(0x00FA9A), "mediumturquoise": vaxis.HexColor(0x48D1CC), "mediumvioletred": vaxis.HexColor(0xC71585), "midnightblue": vaxis.HexColor(0x191970), "mintcream": vaxis.HexColor(0xF5FFFA), "mistyrose": vaxis.HexColor(0xFFE4E1), "moccasin": vaxis.HexColor(0xFFE4B5), "navajowhite": vaxis.HexColor(0xFFDEAD), "oldlace": vaxis.HexColor(0xFDF5E6), "olivedrab": vaxis.HexColor(0x6B8E23), "orange": vaxis.HexColor(0xFFA500), "orangered": vaxis.HexColor(0xFF4500), "orchid": vaxis.HexColor(0xDA70D6), "palegoldenrod": vaxis.HexColor(0xEEE8AA), "palegreen": vaxis.HexColor(0x98FB98), "paleturquoise": vaxis.HexColor(0xAFEEEE), "palevioletred": vaxis.HexColor(0xDB7093), "papayawhip": vaxis.HexColor(0xFFEFD5), "peachpuff": vaxis.HexColor(0xFFDAB9), "peru": vaxis.HexColor(0xCD853F), "pink": vaxis.HexColor(0xFFC0CB), "plum": vaxis.HexColor(0xDDA0DD), "powderblue": vaxis.HexColor(0xB0E0E6), "rebeccapurple": vaxis.HexColor(0x663399), "rosybrown": vaxis.HexColor(0xBC8F8F), "royalblue": vaxis.HexColor(0x4169E1), "saddlebrown": vaxis.HexColor(0x8B4513), "salmon": vaxis.HexColor(0xFA8072), "sandybrown": vaxis.HexColor(0xF4A460), "seagreen": vaxis.HexColor(0x2E8B57), "seashell": vaxis.HexColor(0xFFF5EE), "sienna": vaxis.HexColor(0xA0522D), "skyblue": vaxis.HexColor(0x87CEEB), "slateblue": vaxis.HexColor(0x6A5ACD), "slategray": vaxis.HexColor(0x708090), "snow": vaxis.HexColor(0xFFFAFA), "springgreen": vaxis.HexColor(0x00FF7F), "steelblue": vaxis.HexColor(0x4682B4), "tan": vaxis.HexColor(0xD2B48C), "thistle": vaxis.HexColor(0xD8BFD8), "tomato": vaxis.HexColor(0xFF6347), "turquoise": vaxis.HexColor(0x40E0D0), "violet": vaxis.HexColor(0xEE82EE), "wheat": vaxis.HexColor(0xF5DEB3), "whitesmoke": vaxis.HexColor(0xF5F5F5), "yellowgreen": vaxis.HexColor(0x9ACD32), }