e810ae12d7
Make it possible to specify the color in the style sets by number in addition to the color name. This allows using colors defined by the terminal. Signed-off-by: Tobias Wölfel <tobias.woelfel@mailbox.org> Acked-by: Robin Jarry <robin@jarry.cc>
440 lines
8.8 KiB
Go
440 lines
8.8 KiB
Go
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_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 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 "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()
|
|
}
|