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
This commit is contained in:
Srivathsan Murali 2020-01-23 13:56:48 +01:00 committed by Drew DeVault
parent aa967682bc
commit b2fa5a16f5
7 changed files with 170 additions and 20 deletions

View File

@ -16,6 +16,7 @@ import (
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/go-ini/ini" "github.com/go-ini/ini"
"github.com/imdario/mergo"
"github.com/kyoh86/xdg" "github.com/kyoh86/xdg"
"git.sr.ht/~sircmpwn/aerc/lib/templates" "git.sr.ht/~sircmpwn/aerc/lib/templates"
@ -45,6 +46,18 @@ type UIConfig struct {
CompletionPopovers bool `ini:"completion-popovers"` 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 ( const (
FILTER_MIMETYPE = iota FILTER_MIMETYPE = iota
FILTER_HEADER FILTER_HEADER
@ -112,16 +125,17 @@ type TemplateConfig struct {
} }
type AercConfig struct { type AercConfig struct {
Bindings BindingConfig Bindings BindingConfig
Compose ComposeConfig Compose ComposeConfig
Ini *ini.File `ini:"-"` Ini *ini.File `ini:"-"`
Accounts []AccountConfig `ini:"-"` Accounts []AccountConfig `ini:"-"`
Filters []FilterConfig `ini:"-"` Filters []FilterConfig `ini:"-"`
Viewer ViewerConfig `ini:"-"` Viewer ViewerConfig `ini:"-"`
Triggers TriggersConfig `ini:"-"` Triggers TriggersConfig `ini:"-"`
Ui UIConfig Ui UIConfig
General GeneralConfig ContextualUis []UIConfigContext
Templates TemplateConfig General GeneralConfig
Templates TemplateConfig
} }
// Input: TimestampFormat // Input: TimestampFormat
@ -314,6 +328,55 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
return err 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 triggers, err := file.GetSection("triggers"); err == nil {
if err := triggers.MapTo(&config.Triggers); err != nil { if err := triggers.MapTo(&config.Triggers); err != nil {
return err return err
@ -395,6 +458,8 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
CompletionPopovers: true, CompletionPopovers: true,
}, },
ContextualUis: []UIConfigContext{},
Viewer: ViewerConfig{ Viewer: ViewerConfig{
Pager: "less -R", Pager: "less -R",
Alternatives: []string{"text/plain", "text/html"}, Alternatives: []string{"text/plain", "text/html"},
@ -536,3 +601,28 @@ func parseLayout(layout string) [][]string {
} }
return l 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
}

View File

@ -168,6 +168,46 @@ These options are configured in the *[ui]* section of aerc.conf.
Default: 250ms 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=<AccountName>]*
Adds account specific configuration with the account name.
*[ui:folder=<FolderName>]*
Add folder specific configuration with the folder name.
*[ui:folder~<Regex>]*
Add folder specific configuration for folders whose names match the regular
expression.
*[ui:subject~<Regex>]*
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 ## VIEWER
These options are configured in the *[viewer]* section of aerc.conf. These options are configured in the *[viewer]* section of aerc.conf.

1
go.mod
View File

@ -19,6 +19,7 @@ require (
github.com/golang/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.3.2 // indirect
github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect 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/kyoh86/xdg v1.0.0
github.com/mattn/go-isatty v0.0.8 github.com/mattn/go-isatty v0.0.8
github.com/mattn/go-runewidth v0.0.4 github.com/mattn/go-runewidth v0.0.4

2
go.sum
View File

@ -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-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 h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 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 h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 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= github.com/kyoh86/xdg v1.0.0 h1:TD1layQ0epNApNwGRblnQnT3S/2UH/gCQN1cmXWotvE=

View File

@ -31,13 +31,24 @@ type AccountView struct {
worker *types.Worker 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, func NewAccountView(aerc *Aerc, conf *config.AercConfig, acct *config.AccountConfig,
logger *log.Logger, host TabHost) *AccountView { logger *log.Logger, host TabHost) *AccountView {
acctUiConf := conf.GetUiConfig(map[int]string{
config.UI_CONTEXT_ACCOUNT: acct.Name,
})
grid := ui.NewGrid().Rows([]ui.GridSpec{ grid := ui.NewGrid().Rows([]ui.GridSpec{
{ui.SIZE_WEIGHT, 1}, {ui.SIZE_WEIGHT, 1},
}).Columns([]ui.GridSpec{ }).Columns([]ui.GridSpec{
{ui.SIZE_EXACT, conf.Ui.SidebarWidth}, {ui.SIZE_EXACT, acctUiConf.SidebarWidth},
{ui.SIZE_WEIGHT, 1}, {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) dirlist := NewDirectoryList(acct, &acctUiConf, logger, worker)
if conf.Ui.SidebarWidth > 0 { if acctUiConf.SidebarWidth > 0 {
grid.AddChild(ui.NewBordered(dirlist, ui.BORDER_RIGHT)) 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.Triggers.ExecNewEmail(acct.acct,
acct.conf, msg) acct.conf, msg)
}, func() { }, func() {
if acct.conf.Ui.NewMessageBell { if acct.UiConfig().NewMessageBell {
acct.host.Beep() acct.host.Beep()
} }
}) })
@ -272,10 +283,10 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
} }
func (acct *AccountView) getSortCriteria() []*types.SortCriterion { func (acct *AccountView) getSortCriteria() []*types.SortCriterion {
if len(acct.conf.Ui.Sort) == 0 { if len(acct.UiConfig().Sort) == 0 {
return nil return nil
} }
criteria, err := sort.GetSortCriteria(acct.conf.Ui.Sort) criteria, err := sort.GetSortCriteria(acct.UiConfig().Sort)
if err != nil { if err != nil {
acct.aerc.PushError(" ui.sort: " + err.Error()) acct.aerc.PushError(" ui.sort: " + err.Error())
return nil return nil

View File

@ -106,10 +106,16 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
} }
ctx.Fill(0, row, ctx.Width(), 1, ' ', style) 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( fmtStr, args, err := format.ParseMessageFormat(
ml.aerc.SelectedAccount().acct.From, ml.aerc.SelectedAccount().acct.From,
ml.conf.Ui.IndexFormat, uiConfig.IndexFormat,
ml.conf.Ui.TimestampFormat, "", i, msg, store.IsMarked(uid)) uiConfig.TimestampFormat, "", i, msg, store.IsMarked(uid))
if err != nil { if err != nil {
ctx.Printf(0, row, style, "%v", err) ctx.Printf(0, row, style, "%v", err)
} else { } else {
@ -265,7 +271,7 @@ func (ml *MessageList) Scroll() {
} }
func (ml *MessageList) drawEmptyMessage(ctx *ui.Context) { 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, ctx.Printf((ctx.Width()/2)-(len(msg)/2), 0,
tcell.StyleDefault, "%s", msg) tcell.StyleDefault, "%s", msg)
} }

View File

@ -63,7 +63,7 @@ func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
func(header string) ui.Drawable { func(header string) ui.Drawable {
return &HeaderView{ return &HeaderView{
Name: header, Name: header,
Value: fmtHeader(msg, header, conf.Ui.TimestampFormat), Value: fmtHeader(msg, header, acct.UiConfig().TimestampFormat),
} }
}, },
) )