~sircmpwn/aerc

Add UI Config Specialization v1 PROPOSED

Srivathsan Murali: 1
 Add UI Config Specialization

 7 files changed, 163 insertions(+), 20 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/aerc/patches/9089/mbox | git am -3
Learn more about email & git

[PATCH] Add UI Config Specialization Export this patch

UI Config specialization for accounts and folders.
+ Adds parsing of specialized 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.
+ Add documentation for UI Config Specialization.
---
After following the conversation for adding a UIConfig option to have
index-format for sent folder specifically.

We left off deciding to work on UI Config specializations for folders.
I also realized, it would be useful to have specialization for accounts.

Hence, here is my attempt at adding specialization for UI
Configurations.

 config/config.go      | 117 ++++++++++++++++++++++++++++++++++++++----
 config/triggers.go    |   3 +-
 doc/aerc-config.5.scd |  29 +++++++++++
 go.mod                |   1 +
 go.sum                |   2 +
 widgets/account.go    |  23 ++++++---
 widgets/msglist.go    |   8 +--
 7 files changed, 163 insertions(+), 20 deletions(-)

diff --git a/config/config.go b/config/config.go
index 32d07fca44b7..216c926f95aa 100644
--- a/config/config.go
+++ b/config/config.go
@@ -15,6 +15,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"
@@ -42,6 +43,17 @@ type UIConfig struct {
 	NextMessageOnDelete bool     `ini:"next-message-on-delete"`
 }
 
+const (
+	SPECIAL_UI_FOLDER = iota
+	SPECIAL_UI_ACCOUNT
+)
+
+type UIConfigSpecial struct {
+	SpecialType int
+	Regex       *regexp.Regexp
+	UiConfig    UIConfig
+}
+
 const (
 	FILTER_MIMETYPE = iota
 	FILTER_HEADER
@@ -107,16 +119,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
+	SpecialUis []UIConfigSpecial
+	General    GeneralConfig
+	Templates  TemplateConfig
 }
 
 // Input: TimestampFormat
@@ -309,6 +322,52 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
 			return err
 		}
 	}
+
+	for _, sectionName := range file.SectionStrings() {
+		if !strings.Contains(sectionName, "ui:") {
+			continue
+		}
+
+		subUi, err := file.GetSection(sectionName)
+		if err != nil {
+			return err
+		}
+		uiSubConfig := UIConfig{}
+		if err := subUi.MapTo(&uiSubConfig); err != nil {
+			return err
+		}
+		specialUi :=
+			UIConfigSpecial{
+				UiConfig: uiSubConfig,
+			}
+
+		if strings.Contains(sectionName, "ui:folder=") {
+			specialUi.SpecialType = SPECIAL_UI_FOLDER
+			value := string(sectionName[strings.Index(sectionName, "=")+1:])
+			specialUi.Regex, err = regexp.Compile(regexp.QuoteMeta(value))
+			if err != nil {
+				return err
+			}
+		} else if strings.Contains(sectionName, "ui:folder~") {
+			specialUi.SpecialType = SPECIAL_UI_FOLDER
+			regex := string(sectionName[strings.Index(sectionName, "~")+1:])
+			specialUi.Regex, err = regexp.Compile(regex)
+			if err != nil {
+				return err
+			}
+		} else if strings.Contains(sectionName, "ui:account=") {
+			specialUi.SpecialType = SPECIAL_UI_ACCOUNT
+			value := string(sectionName[strings.Index(sectionName, "=")+1:])
+			specialUi.Regex, err = regexp.Compile(regexp.QuoteMeta(value))
+			if err != nil {
+				return err
+			}
+		} else {
+			return fmt.Errorf("Unknown Special Ui Section: %s", sectionName)
+		}
+		config.SpecialUis = append(config.SpecialUis, specialUi)
+	}
+
 	if triggers, err := file.GetSection("triggers"); err == nil {
 		if err := triggers.MapTo(&config.Triggers); err != nil {
 			return err
@@ -388,6 +447,8 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
 			NextMessageOnDelete: true,
 		},
 
+		SpecialUis: []UIConfigSpecial{},
+
 		Viewer: ViewerConfig{
 			Pager:        "less -R",
 			Alternatives: []string{"text/plain", "text/html"},
@@ -529,3 +590,39 @@ func parseLayout(layout string) [][]string {
 	}
 	return l
 }
+
+func (config *AercConfig) getAccountConfig(baseUi *UIConfig, account string) {
+	for _, sui := range config.SpecialUis {
+		switch sui.SpecialType {
+		case SPECIAL_UI_ACCOUNT:
+			if sui.Regex.Match([]byte(account)) {
+				mergo.MergeWithOverwrite(baseUi, sui.UiConfig)
+			}
+		}
+	}
+}
+
+func (config *AercConfig) getFolderConfig(baseUi *UIConfig, folder string) {
+	for _, sui := range config.SpecialUis {
+		switch sui.SpecialType {
+		case SPECIAL_UI_FOLDER:
+			if sui.Regex.Match([]byte(folder)) {
+				mergo.MergeWithOverwrite(baseUi, sui.UiConfig)
+			}
+		}
+	}
+}
+
+func (config *AercConfig) GetUiConfig(params map[string]string) UIConfig {
+	baseUi := config.Ui
+
+	if accountName, ok := params["accountName"]; ok {
+		config.getAccountConfig(&baseUi, accountName)
+	}
+
+	if folderName, ok := params["folder"]; ok {
+		config.getFolderConfig(&baseUi, folderName)
+	}
+
+	return baseUi
+}
diff --git a/config/triggers.go b/config/triggers.go
index f68cb58504b3..532a83cece40 100644
--- a/config/triggers.go
+++ b/config/triggers.go
@@ -34,12 +34,13 @@ func (trig *TriggersConfig) ExecTrigger(triggerCmd string,
 
 func (trig *TriggersConfig) ExecNewEmail(account *AccountConfig,
 	conf *AercConfig, msg *models.MessageInfo) {
+	uiConf := conf.GetUiConfig(map[string]string{"accountName": account.Name})
 	err := trig.ExecTrigger(trig.NewEmail,
 		func(part string) (string, error) {
 			formatstr, args, err := format.ParseMessageFormat(
 				account.From,
 				part,
-				conf.Ui.TimestampFormat, account.Name, 0, msg)
+				uiConf.TimestampFormat, account.Name, 0, msg)
 			if err != nil {
 				return "", err
 			}
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index 02fe4d657f8f..6abc88217514 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -156,6 +156,35 @@ These options are configured in the *[ui]* section of aerc.conf.
 
 	Default: true
 
+## UI Config Specialization
+
+The UI configuration can be specialized for accounts and specific mail
+directories. The specializations are added using config sections which
+match the names. The folder specialization are merged into the generic
+UI Configuration after the account specialization.
+
+*[ui:account=<AccountName>]*
+	Adds UI specializations for account with the specified name.
+
+*[ui:folder=<FolderName>]*
+	Add UI specialization for mail directory with the specified name.
+
+*[ui:folder~<Regex>]*
+	Add UI specialization for mail directories whose names match the regex
+	specified as part of the section name.
+
+Example:
+```
+[ui:account=Work]
+sidebar-width=...
+
+[ui:folder=Sent]
+index-format=...
+
+[ui:folder~Archive/\d+/.*]
+index-format=...
+```
+
 ## VIEWER
 
 These options are configured in the *[viewer]* section of aerc.conf.
diff --git a/go.mod b/go.mod
index 0595a0f0a971..40a0c990bfa1 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 f111fc5ec8bb..e991d928dd88 100644
--- a/go.sum
+++ b/go.sum
@@ -50,6 +50,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 4e8dd171b51f..e240f0c1c148 100644
--- a/widgets/account.go
+++ b/widgets/account.go
@@ -28,13 +28,24 @@ type AccountView struct {
 	worker  *types.Worker
 }
 
+func (acct *AccountView) UiConfig() config.UIConfig {
+	return acct.conf.GetUiConfig(map[string]string{
+		"accountName": acct.AccountConfig().Name,
+		"folder":      acct.Directories().Selected(),
+	})
+}
+
 func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountConfig,
 	logger *log.Logger, host TabHost) *AccountView {
 
+	acctUiConf := conf.GetUiConfig(map[string]string{
+		"accountName": 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},
 	})
 
@@ -51,8 +62,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))
 	}
 
@@ -224,7 +235,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()
 					}
 				})
@@ -258,10 +269,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 aed3ed53cb77..bbffbc249b1a 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -106,10 +106,12 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
 		}
 
 		ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
+		uiConfig := ml.aerc.SelectedAccount().UiConfig()
+
 		fmtStr, args, err := format.ParseMessageFormat(
 			ml.aerc.SelectedAccount().acct.From,
-			ml.conf.Ui.IndexFormat,
-			ml.conf.Ui.TimestampFormat, "", i, msg)
+			uiConfig.IndexFormat,
+			uiConfig.TimestampFormat, "", i, msg)
 		if err != nil {
 			ctx.Printf(0, row, style, "%v", err)
 		} else {
@@ -265,7 +267,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)
 }
-- 
2.24.0
General feedback:

- let's call these kinds of configs "contextual" rather than special
- can we also see a proof-of-concept for changing the context based on
  the message? For example using subject matching to change the index
  format for patches

Otherwise this looks pretty good.
View this thread in the archives