From b2fa5a16f52741a6f7f6e5f33561457d702dc31d Mon Sep 17 00:00:00 2001 From: Srivathsan Murali Date: Thu, 23 Jan 2020 13:56:48 +0100 Subject: [PATCH] Contextual UI Configuration + Adds parsing of contextual ui sections to aerc config. + Add GetUiConfig method for AercConfig that is used to get the specialized UI config. + Add UiConfig method to AccountView to get specialized UI Config. + Modifies Aerc codebase to use specialized UIConfig instead. + Adds documentation for Contextual UI Configuration --- config/config.go | 110 ++++++++++++++++++++++++++++++++++++++---- doc/aerc-config.5.scd | 40 +++++++++++++++ go.mod | 1 + go.sum | 2 + widgets/account.go | 23 ++++++--- widgets/msglist.go | 12 +++-- widgets/msgviewer.go | 2 +- 7 files changed, 170 insertions(+), 20 deletions(-) diff --git a/config/config.go b/config/config.go index fe548ff..0b46014 100644 --- a/config/config.go +++ b/config/config.go @@ -16,6 +16,7 @@ import ( "github.com/gdamore/tcell" "github.com/go-ini/ini" + "github.com/imdario/mergo" "github.com/kyoh86/xdg" "git.sr.ht/~sircmpwn/aerc/lib/templates" @@ -45,6 +46,18 @@ type UIConfig struct { CompletionPopovers bool `ini:"completion-popovers"` } +const ( + UI_CONTEXT_FOLDER = iota + UI_CONTEXT_ACCOUNT + UI_CONTEXT_SUBJECT +) + +type UIConfigContext struct { + ContextType int + Regex *regexp.Regexp + UiConfig UIConfig +} + const ( FILTER_MIMETYPE = iota FILTER_HEADER @@ -112,16 +125,17 @@ type TemplateConfig struct { } type AercConfig struct { - Bindings BindingConfig - Compose ComposeConfig - Ini *ini.File `ini:"-"` - Accounts []AccountConfig `ini:"-"` - Filters []FilterConfig `ini:"-"` - Viewer ViewerConfig `ini:"-"` - Triggers TriggersConfig `ini:"-"` - Ui UIConfig - General GeneralConfig - Templates TemplateConfig + Bindings BindingConfig + Compose ComposeConfig + Ini *ini.File `ini:"-"` + Accounts []AccountConfig `ini:"-"` + Filters []FilterConfig `ini:"-"` + Viewer ViewerConfig `ini:"-"` + Triggers TriggersConfig `ini:"-"` + Ui UIConfig + ContextualUis []UIConfigContext + General GeneralConfig + Templates TemplateConfig } // Input: TimestampFormat @@ -314,6 +328,55 @@ func (config *AercConfig) LoadConfig(file *ini.File) error { return err } } + for _, sectionName := range file.SectionStrings() { + if !strings.Contains(sectionName, "ui:") { + continue + } + + uiSection, err := file.GetSection(sectionName) + if err != nil { + return err + } + uiSubConfig := UIConfig{} + if err := uiSection.MapTo(&uiSubConfig); err != nil { + return err + } + contextualUi := + UIConfigContext{ + UiConfig: uiSubConfig, + } + + var index int + if strings.Contains(sectionName, "~") { + index = strings.Index(sectionName, "~") + regex := string(sectionName[index+1:]) + contextualUi.Regex, err = regexp.Compile(regex) + if err != nil { + return err + } + } else if strings.Contains(sectionName, "=") { + index = strings.Index(sectionName, "=") + value := string(sectionName[index+1:]) + contextualUi.Regex, err = regexp.Compile(regexp.QuoteMeta(value)) + if err != nil { + return err + } + } else { + return fmt.Errorf("Invalid Ui Context regex in %s", sectionName) + } + + switch sectionName[3:index] { + case "account": + contextualUi.ContextType = UI_CONTEXT_ACCOUNT + case "folder": + contextualUi.ContextType = UI_CONTEXT_FOLDER + case "subject": + contextualUi.ContextType = UI_CONTEXT_SUBJECT + default: + return fmt.Errorf("Unknown Contextual Ui Section: %s", sectionName) + } + config.ContextualUis = append(config.ContextualUis, contextualUi) + } if triggers, err := file.GetSection("triggers"); err == nil { if err := triggers.MapTo(&config.Triggers); err != nil { return err @@ -395,6 +458,8 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) { CompletionPopovers: true, }, + ContextualUis: []UIConfigContext{}, + Viewer: ViewerConfig{ Pager: "less -R", Alternatives: []string{"text/plain", "text/html"}, @@ -536,3 +601,28 @@ func parseLayout(layout string) [][]string { } return l } + +func (config *AercConfig) mergeContextualUi(baseUi *UIConfig, contextType int, s string) { + for _, contextualUi := range config.ContextualUis { + if contextualUi.ContextType != contextType { + continue + } + + if !contextualUi.Regex.Match([]byte(s)) { + continue + } + + mergo.MergeWithOverwrite(baseUi, contextualUi.UiConfig) + return + } +} + +func (config *AercConfig) GetUiConfig(params map[int]string) UIConfig { + baseUi := config.Ui + + for k, v := range params { + config.mergeContextualUi(&baseUi, k, v) + } + + return baseUi +} diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd index 791a39d..c747c61 100644 --- a/doc/aerc-config.5.scd +++ b/doc/aerc-config.5.scd @@ -168,6 +168,46 @@ These options are configured in the *[ui]* section of aerc.conf. Default: 250ms +## Contextual UI Configuration + +The UI configuration can be specialized for accounts, specific mail +directories and message subjects. The specializations are added using +contextual config sections based on the context. + +The contextual UI configuration is merged to the base UiConfig in the +following order: +*Base UIConfig > Account Context > Folder Context > Subject Context.* + +*[ui:account=]* + Adds account specific configuration with the account name. + +*[ui:folder=]* + Add folder specific configuration with the folder name. + +*[ui:folder~]* + Add folder specific configuration for folders whose names match the regular + expression. + +*[ui:subject~]* + Add specialized ui configuration for messages that match a given regular + expression. + +Example: +``` +[ui:account=Work] +sidebar-width=... + +[ui:folder=Sent] +index-format=... + +[ui:folder~Archive/\d+/.*] +index-format=... + +[ui:subject~^\[PATCH] +index-format=... +``` + + ## VIEWER These options are configured in the *[viewer]* section of aerc.conf. diff --git a/go.mod b/go.mod index 824185e..c7839fd 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/golang/protobuf v1.3.2 // indirect github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect + github.com/imdario/mergo v0.3.8 github.com/kyoh86/xdg v1.0.0 github.com/mattn/go-isatty v0.0.8 github.com/mattn/go-runewidth v0.0.4 diff --git a/go.sum b/go.sum index 119d317..01024b4 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kyoh86/xdg v1.0.0 h1:TD1layQ0epNApNwGRblnQnT3S/2UH/gCQN1cmXWotvE= diff --git a/widgets/account.go b/widgets/account.go index 404a9ea..66320a3 100644 --- a/widgets/account.go +++ b/widgets/account.go @@ -31,13 +31,24 @@ type AccountView struct { worker *types.Worker } +func (acct *AccountView) UiConfig() config.UIConfig { + return acct.conf.GetUiConfig(map[int]string{ + config.UI_CONTEXT_ACCOUNT: acct.AccountConfig().Name, + config.UI_CONTEXT_FOLDER: acct.Directories().Selected(), + }) +} + func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountConfig, logger *log.Logger, host TabHost) *AccountView { + acctUiConf := conf.GetUiConfig(map[int]string{ + config.UI_CONTEXT_ACCOUNT: acct.Name, + }) + grid := ui.NewGrid().Rows([]ui.GridSpec{ {ui.SIZE_WEIGHT, 1}, }).Columns([]ui.GridSpec{ - {ui.SIZE_EXACT, conf.Ui.SidebarWidth}, + {ui.SIZE_EXACT, acctUiConf.SidebarWidth}, {ui.SIZE_WEIGHT, 1}, }) @@ -54,8 +65,8 @@ func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountCon } } - dirlist := NewDirectoryList(acct, &conf.Ui, logger, worker) - if conf.Ui.SidebarWidth > 0 { + dirlist := NewDirectoryList(acct, &acctUiConf, logger, worker) + if acctUiConf.SidebarWidth > 0 { grid.AddChild(ui.NewBordered(dirlist, ui.BORDER_RIGHT)) } @@ -236,7 +247,7 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) { acct.conf.Triggers.ExecNewEmail(acct.acct, acct.conf, msg) }, func() { - if acct.conf.Ui.NewMessageBell { + if acct.UiConfig().NewMessageBell { acct.host.Beep() } }) @@ -272,10 +283,10 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) { } func (acct *AccountView) getSortCriteria() []*types.SortCriterion { - if len(acct.conf.Ui.Sort) == 0 { + if len(acct.UiConfig().Sort) == 0 { return nil } - criteria, err := sort.GetSortCriteria(acct.conf.Ui.Sort) + criteria, err := sort.GetSortCriteria(acct.UiConfig().Sort) if err != nil { acct.aerc.PushError(" ui.sort: " + err.Error()) return nil diff --git a/widgets/msglist.go b/widgets/msglist.go index 243c5db..24a9940 100644 --- a/widgets/msglist.go +++ b/widgets/msglist.go @@ -106,10 +106,16 @@ func (ml *MessageList) Draw(ctx *ui.Context) { } ctx.Fill(0, row, ctx.Width(), 1, ' ', style) + uiConfig := ml.conf.GetUiConfig(map[int]string{ + config.UI_CONTEXT_ACCOUNT: ml.aerc.SelectedAccount().AccountConfig().Name, + config.UI_CONTEXT_FOLDER: ml.aerc.SelectedAccount().Directories().Selected(), + config.UI_CONTEXT_SUBJECT: msg.Envelope.Subject, + }) + fmtStr, args, err := format.ParseMessageFormat( ml.aerc.SelectedAccount().acct.From, - ml.conf.Ui.IndexFormat, - ml.conf.Ui.TimestampFormat, "", i, msg, store.IsMarked(uid)) + uiConfig.IndexFormat, + uiConfig.TimestampFormat, "", i, msg, store.IsMarked(uid)) if err != nil { ctx.Printf(0, row, style, "%v", err) } else { @@ -265,7 +271,7 @@ func (ml *MessageList) Scroll() { } func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) { - msg := ml.conf.Ui.EmptyMessage + msg := ml.aerc.SelectedAccount().UiConfig().EmptyMessage ctx.Printf((ctx.Width()/2)-(len(msg)/2), 0, tcell.StyleDefault, "%s", msg) } diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go index 0bfd2d8..93d3d89 100644 --- a/widgets/msgviewer.go +++ b/widgets/msgviewer.go @@ -63,7 +63,7 @@ func NewMessageViewer(acct *AccountView, conf *config.AercConfig, func(header string) ui.Drawable { return &HeaderView{ Name: header, - Value: fmtHeader(msg, header, conf.Ui.TimestampFormat), + Value: fmtHeader(msg, header, acct.UiConfig().TimestampFormat), } }, )