package config import ( "errors" "fmt" "os" "path" "regexp" "strconv" "strings" "github.com/gdamore/tcell/v2" "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_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_DIRLIST_DEFAULT STYLE_DIRLIST_UNREAD STYLE_DIRLIST_RECENT 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, "dirlist_default": STYLE_DIRLIST_DEFAULT, "dirlist_unread": STYLE_DIRLIST_UNREAD, "dirlist_recent": STYLE_DIRLIST_RECENT, "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 Italic bool Dim bool } func (s Style) Get() tcell.Style { return tcell.StyleDefault. Foreground(s.Fg). Background(s.Bg). Bold(s.Bold). Blink(s.Blink). Underline(s.Underline). Reverse(s.Reverse). Italic(s.Italic). Dim(s.Dim) } 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 = 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 extractColor(val string) tcell.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 tcell.PaletteColor(int(i)) } else { return tcell.GetColor(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 { newStyle.Fg = st.Fg } if st.Bg != s.Bg { 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 StyleSet struct { objects map[StyleObject]*Style selected map[StyleObject]*Style user map[string]*Style path string } func NewStyleSet() StyleSet { ss := StyleSet{ objects: make(map[StyleObject]*Style), selected: make(map[StyleObject]*Style), user: make(map[string]*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 (ss StyleSet) UserStyle(name string) tcell.Style { return ss.user[name].Get() } func (ss StyleSet) Compose(so StyleObject, sos []StyleObject) tcell.Style { base := *ss.objects[so] styles := make([]*Style, len(sos)) for i, so := range sos { styles[i] = ss.objects[so] } return base.composeWith(styles).Get() } func (ss StyleSet) ComposeSelected(so StyleObject, sos []StyleObject, ) tcell.Style { base := *ss.selected[so] styles := make([]*Style, len(sos)) for i, so := range sos { styles[i] = ss.selected[so] } return base.composeWith(styles).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 } } } 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 err } } 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) 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() }