summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobin Jarry <robin@jarry.cc>2023-03-11 20:42:55 +0100
committerRobin Jarry <robin@jarry.cc>2023-03-31 21:02:23 +0200
commit2f46f64b0b0b93e99b4754a566c84a08d4563078 (patch)
treea91261379109115c6116e1106ec3e37b62eba048
parent47675e80850d981b19c6fb231fbebaf5674f3682 (diff)
downloadaerc-2f46f64b0b0b93e99b4754a566c84a08d4563078.tar.gz
aerc-2f46f64b0b0b93e99b4754a566c84a08d4563078.zip
styleset: allow dynamic msglist styling
Add support for dynamic msglist*.$HEADER,$VALUE.$ATTR = $VALUE where $VALUE can be either a fixed string or a regular expression. This is intended as a replacement of contextual ui sections based on subject values. Implements: https://todo.sr.ht/~rjarry/aerc/18 Signed-off-by: Robin Jarry <robin@jarry.cc> Tested-by: Bence Ferdinandy <bence@ferdinandy.com>
-rw-r--r--CHANGELOG.md1
-rw-r--r--config/style.go112
-rw-r--r--config/ui.go21
-rw-r--r--doc/aerc-stylesets.7.scd20
-rw-r--r--widgets/msglist.go13
5 files changed, 134 insertions, 33 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e2d7ba47..f126d485 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Allow configuring URL handlers via `x-scheme-handler/<scheme>` `[openers]` in
`aerc.conf`.
- Allow basic shell globbing in `[openers]` MIME types.
+- Dynamic `msglist_*` styling based on email header values in stylesets.
### Changed
diff --git a/config/style.go b/config/style.go
index c8134585..79bd69d1 100644
--- a/config/style.go
+++ b/config/style.go
@@ -9,6 +9,7 @@ import (
"strconv"
"strings"
+ "github.com/emersion/go-message/mail"
"github.com/gdamore/tcell/v2"
"github.com/go-ini/ini"
"github.com/mitchellh/go-homedir"
@@ -107,6 +108,9 @@ type Style struct {
Reverse bool
Italic bool
Dim bool
+ header string // only for msglist
+ pattern string // only for msglist
+ re *regexp.Regexp // only for msglist
}
func (s Style) Get() tcell.Style {
@@ -251,40 +255,64 @@ func (s Style) composeWith(styles []*Style) Style {
return newStyle
}
+type StyleConf struct {
+ base Style
+ dynamic []Style
+}
+
type StyleSet struct {
- objects map[StyleObject]*Style
- selected map[StyleObject]*Style
+ objects map[StyleObject]*StyleConf
+ selected map[StyleObject]*StyleConf
user map[string]*Style
path string
}
func NewStyleSet() StyleSet {
ss := StyleSet{
- objects: make(map[StyleObject]*Style),
- selected: make(map[StyleObject]*Style),
+ objects: make(map[StyleObject]*StyleConf),
+ selected: make(map[StyleObject]*StyleConf),
user: make(map[string]*Style),
}
for _, so := range StyleNames {
- ss.objects[so] = new(Style)
- ss.selected[so] = new(Style)
+ ss.objects[so] = new(StyleConf)
+ ss.selected[so] = new(StyleConf)
}
-
return ss
}
func (ss StyleSet) reset() {
for _, so := range StyleNames {
- ss.objects[so].Reset()
- ss.selected[so].Reset()
+ ss.objects[so].base.Reset()
+ for _, d := range ss.objects[so].dynamic {
+ d.Reset()
+ }
+ ss.selected[so].base.Reset()
+ for _, d := range ss.selected[so].dynamic {
+ d.Reset()
+ }
}
}
-func (ss StyleSet) Get(so StyleObject) tcell.Style {
- return ss.objects[so].Get()
+func (c *StyleConf) getStyle(h *mail.Header) *Style {
+ if h == nil {
+ return &c.base
+ }
+ for _, s := range c.dynamic {
+ val, _ := h.Text(s.header)
+ if s.re.MatchString(val) {
+ s = c.base.composeWith([]*Style{&s})
+ return &s
+ }
+ }
+ return &c.base
+}
+
+func (ss StyleSet) Get(so StyleObject, h *mail.Header) tcell.Style {
+ return ss.objects[so].getStyle(h).Get()
}
-func (ss StyleSet) Selected(so StyleObject) tcell.Style {
- return ss.selected[so].Get()
+func (ss StyleSet) Selected(so StyleObject, h *mail.Header) tcell.Style {
+ return ss.selected[so].getStyle(h).Get()
}
func (ss StyleSet) UserStyle(name string) tcell.Style {
@@ -294,23 +322,25 @@ func (ss StyleSet) UserStyle(name string) tcell.Style {
return tcell.StyleDefault
}
-func (ss StyleSet) Compose(so StyleObject, sos []StyleObject) tcell.Style {
- base := *ss.objects[so]
+func (ss StyleSet) Compose(
+ so StyleObject, sos []StyleObject, h *mail.Header,
+) tcell.Style {
+ base := *ss.objects[so].getStyle(h)
styles := make([]*Style, len(sos))
for i, so := range sos {
- styles[i] = ss.objects[so]
+ styles[i] = ss.objects[so].getStyle(h)
}
return base.composeWith(styles).Get()
}
-func (ss StyleSet) ComposeSelected(so StyleObject,
- sos []StyleObject,
+func (ss StyleSet) ComposeSelected(
+ so StyleObject, sos []StyleObject, h *mail.Header,
) tcell.Style {
- base := *ss.selected[so]
+ base := *ss.selected[so].getStyle(h)
styles := make([]*Style, len(sos))
for i, so := range sos {
- styles[i] = ss.selected[so]
+ styles[i] = ss.selected[so].getStyle(h)
}
return base.composeWith(styles).Get()
@@ -386,17 +416,18 @@ func (ss *StyleSet) ParseStyleSet(file *ini.File) error {
return nil
}
-var styleObjRe = regexp.MustCompile(`^([\w\*\?]+)(\.selected)?\.(\w+)$`)
+var styleObjRe = regexp.MustCompile(`^([\w\*\?]+)(?:\.([\w-]+),(.+?))?(\.selected)?\.(\w+)$`)
func (ss *StyleSet) parseKey(key *ini.Key, selected bool) error {
groups := styleObjRe.FindStringSubmatch(key.Name())
if groups == nil {
return errors.New("invalid style syntax: " + key.Name())
}
- if groups[2] == ".selected" && !selected {
+ if groups[4] == ".selected" && !selected {
return nil
}
- obj, attr := groups[1], groups[3]
+ obj, attr := groups[1], groups[5]
+ header, pattern := groups[2], groups[3]
objRe, err := fnmatchToRegex(obj)
if err != nil {
@@ -408,12 +439,12 @@ func (ss *StyleSet) parseKey(key *ini.Key, selected bool) error {
continue
}
if !selected {
- err = ss.objects[so].Set(attr, key.Value())
+ err = ss.objects[so].update(header, pattern, attr, key.Value())
if err != nil {
return err
}
}
- err = ss.selected[so].Set(attr, key.Value())
+ err = ss.selected[so].update(header, pattern, attr, key.Value())
if err != nil {
return err
}
@@ -425,6 +456,37 @@ func (ss *StyleSet) parseKey(key *ini.Key, selected bool) error {
return nil
}
+func (c *StyleConf) update(header, pattern, attr, val string) error {
+ if header == "" || pattern == "" {
+ return (&c.base).Set(attr, val)
+ }
+ for i := range c.dynamic {
+ s := &c.dynamic[i]
+ if s.header == header && s.pattern == pattern {
+ return s.Set(attr, val)
+ }
+ }
+ if strings.HasPrefix(pattern, "~") {
+ pattern = pattern[1:]
+ } else {
+ pattern = "^" + regexp.QuoteMeta(pattern) + "$"
+ }
+ re, err := regexp.Compile(pattern)
+ if err != nil {
+ return err
+ }
+ var s Style
+ err = (&s).Set(attr, val)
+ if err != nil {
+ return err
+ }
+ s.header = header
+ s.pattern = pattern
+ s.re = re
+ c.dynamic = append(c.dynamic, s)
+ return nil
+}
+
func (ss *StyleSet) LoadStyleSet(stylesetName string, stylesetDirs []string) error {
filepath, err := findStyleSet(stylesetName, stylesetDirs)
if err != nil {
diff --git a/config/ui.go b/config/ui.go
index c67502da..56a59e74 100644
--- a/config/ui.go
+++ b/config/ui.go
@@ -11,6 +11,7 @@ import (
"git.sr.ht/~rjarry/aerc/lib/templates"
"git.sr.ht/~rjarry/aerc/log"
+ "github.com/emersion/go-message/mail"
"github.com/gdamore/tcell/v2"
"github.com/go-ini/ini"
"github.com/imdario/mergo"
@@ -485,23 +486,35 @@ func (uiConfig *UIConfig) GetUserStyle(name string) tcell.Style {
}
func (uiConfig *UIConfig) GetStyle(so StyleObject) tcell.Style {
- return uiConfig.style.Get(so)
+ return uiConfig.style.Get(so, nil)
}
func (uiConfig *UIConfig) GetStyleSelected(so StyleObject) tcell.Style {
- return uiConfig.style.Selected(so)
+ return uiConfig.style.Selected(so, nil)
}
func (uiConfig *UIConfig) GetComposedStyle(base StyleObject,
styles []StyleObject,
) tcell.Style {
- return uiConfig.style.Compose(base, styles)
+ return uiConfig.style.Compose(base, styles, nil)
}
func (uiConfig *UIConfig) GetComposedStyleSelected(
base StyleObject, styles []StyleObject,
) tcell.Style {
- return uiConfig.style.ComposeSelected(base, styles)
+ return uiConfig.style.ComposeSelected(base, styles, nil)
+}
+
+func (uiConfig *UIConfig) MsgComposedStyle(
+ base StyleObject, styles []StyleObject, h *mail.Header,
+) tcell.Style {
+ return uiConfig.style.Compose(base, styles, h)
+}
+
+func (uiConfig *UIConfig) MsgComposedStyleSelected(
+ base StyleObject, styles []StyleObject, h *mail.Header,
+) tcell.Style {
+ return uiConfig.style.ComposeSelected(base, styles, h)
}
func (uiConfig *UIConfig) StyleSetPath() string {
diff --git a/doc/aerc-stylesets.7.scd b/doc/aerc-stylesets.7.scd
index 61d6843d..c9f0a78f 100644
--- a/doc/aerc-stylesets.7.scd
+++ b/doc/aerc-stylesets.7.scd
@@ -248,6 +248,26 @@ The order for *dirlist_\** styles is:
. *dirlist_unread*
. *dirlist_recent*
+# DYNAMIC MESSAGE LIST STYLES
+
+All *msglist_\** styles can be defined for specific email header values. The
+syntax is as follows:
+
+ *msglist_<name>*._<header>_,_<header_value>_.*<attribute>* = _<attr_value>_
+
+If _<header_value>_ starts with a tilde character _~_, it will be interpreted as
+a regular expression.
+
+Examples:
+
+```
+msglist\*.X-Sourcehut-Patchset-Update,APPROVED.fg = green
+msglist\*.X-Sourcehut-Patchset-Update,NEEDS\_REVISION.fg = yellow
+msglist\*.X-Sourcehut-Patchset-Update,REJECTED.fg = red
+"msglist_*.Subject,~^(\[[\w-]+\]\s*)?\[(RFC )?PATCH.fg" = #ffffaf
+"msglist_*.Subject,~^(\[[\w-]+\]\s*)?\[(RFC )?PATCH.selected.fg" = #ffffaf
+```
+
# COLORS
The color values are set using the values accepted by the tcell library.
diff --git a/widgets/msglist.go b/widgets/msglist.go
index 4bf20b09..0937b786 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -7,6 +7,7 @@ import (
"strings"
sortthread "github.com/emersion/go-imap-sortthread"
+ "github.com/emersion/go-message/mail"
"github.com/gdamore/tcell/v2"
"git.sr.ht/~rjarry/aerc/config"
@@ -50,6 +51,7 @@ type messageRowParams struct {
needsHeaders bool
uiConfig *config.UIConfig
styles []config.StyleObject
+ headers *mail.Header
}
func (ml *MessageList) Draw(ctx *ui.Context) {
@@ -110,11 +112,13 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
row := &t.Rows[r]
params, _ := row.Priv.(messageRowParams)
if params.uid == store.SelectedUid() {
- style = params.uiConfig.GetComposedStyleSelected(
- config.STYLE_MSGLIST_DEFAULT, params.styles)
+ style = params.uiConfig.MsgComposedStyleSelected(
+ config.STYLE_MSGLIST_DEFAULT, params.styles,
+ params.headers)
} else {
- style = params.uiConfig.GetComposedStyle(
- config.STYLE_MSGLIST_DEFAULT, params.styles)
+ style = params.uiConfig.MsgComposedStyle(
+ config.STYLE_MSGLIST_DEFAULT, params.styles,
+ params.headers)
}
return style
}
@@ -266,6 +270,7 @@ func addMessage(
// TODO deprecate subject contextual UIs? Only related setting is
// styleset, should implement a better per-message styling method
params.uiConfig = uiConfig.ForSubject(msg.Envelope.Subject)
+ params.headers = msg.RFC822Headers
return table.AddRow(cells, params)
}