aerc/config/style.go
y0ast 03650474e2 update tcell to v2 and enable TrueColor support
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.
2020-12-18 07:23:22 +01:00

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