aboutsummaryrefslogblamecommitdiffstats
path: root/config/style.go
blob: 7b13c3a2a0c1e522b91ea51f9e494c9a3209e110 (plain) (tree)
1
2
3
4
5
6
7
8
9







                
                 

                 
                                     
















                                         
                                




                                
                             

                             
                            
                              

                             

                            

























                                                       
                                                       

                                                       







                                                   

                                                 

                                                





















                                                       
                      
                      







                                  
                                       
                                   

                                 






                           
                        
                     



























                                                                               










                                                                             


                                             
                                        
                  
                                        























                                                                           





                                                                        





                                                                     










                                                                     




















                                                         


                                                   


                                             



                       


                                       
                                  
                       





                                                       
                                                  























                                                         
                                                       



                                                 

 










                                                                           

                          








                                             

































































































































                                                                                       

























                                                                           








                                                                                    



                                                       



                          

                          



















                                                   
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 {
	if style, found := ss.user[name]; found {
		return style.Get()
	}
	return tcell.StyleDefault
}

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()
}