03650474e2
Also update to the tcell v2 PaletteColor api, which should keep the chosen theme of the user intact. Note, that if $TRUECOLOR is defined and a truecolor given, aerc will now stop clipping the value to one of the theme colors. Generally this is desired behaviour though.
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/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_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()
|
|
}
|