diff options
author | Robin Jarry <robin@jarry.cc> | 2023-03-11 20:42:55 +0100 |
---|---|---|
committer | Robin Jarry <robin@jarry.cc> | 2023-03-31 21:02:23 +0200 |
commit | 2f46f64b0b0b93e99b4754a566c84a08d4563078 (patch) | |
tree | a91261379109115c6116e1106ec3e37b62eba048 | |
parent | 47675e80850d981b19c6fb231fbebaf5674f3682 (diff) | |
download | aerc-2f46f64b0b0b93e99b4754a566c84a08d4563078.tar.gz |
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.md | 1 | ||||
-rw-r--r-- | config/style.go | 112 | ||||
-rw-r--r-- | config/ui.go | 21 | ||||
-rw-r--r-- | doc/aerc-stylesets.7.scd | 20 | ||||
-rw-r--r-- | widgets/msglist.go | 13 |
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) } |