f9bba3d17d
Allow styles to be layered over a base style. The list of styles to apply is layered over the base style in order, such that if the layer does not differ from the base it is not used. The order that these styles are applied in is, from first to last: msglist_default msglist_unread msglist_read (exclusive with unread, so technically the same level) msglist_flagged msglist_deleted msglist_marked So, msglist_marked style dominates. This fixes an issue where the msglist_deleted style was not being applied.
428 lines
8.4 KiB
Go
428 lines
8.4 KiB
Go
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_FLAGGED
|
|
STYLE_MSGLIST_DELETED
|
|
STYLE_MSGLIST_MARKED
|
|
|
|
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_flagged": STYLE_MSGLIST_FLAGGED,
|
|
"msglist_deleted": STYLE_MSGLIST_DELETED,
|
|
"msglist_marked": STYLE_MSGLIST_MARKED,
|
|
|
|
"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.Underline).
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
return newStyle
|
|
}
|
|
|
|
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 (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
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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()
|
|
}
|