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:
parent
aa967682bc
commit
b2fa5a16f5
7 changed files with 170 additions and 20 deletions
|
@ -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
|
||||||
|
@ -120,6 +133,7 @@ type AercConfig struct {
|
||||||
Viewer ViewerConfig `ini:"-"`
|
Viewer ViewerConfig `ini:"-"`
|
||||||
Triggers TriggersConfig `ini:"-"`
|
Triggers TriggersConfig `ini:"-"`
|
||||||
Ui UIConfig
|
Ui UIConfig
|
||||||
|
ContextualUis []UIConfigContext
|
||||||
General GeneralConfig
|
General GeneralConfig
|
||||||
Templates TemplateConfig
|
Templates TemplateConfig
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
1
go.mod
|
@ -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
2
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-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=
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue