aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config/accounts.go180
-rw-r--r--config/compose.go54
-rw-r--r--config/config.go17
-rw-r--r--config/general.go46
-rw-r--r--config/parse.go233
-rw-r--r--config/statusline.go77
-rw-r--r--config/templates.go28
-rw-r--r--config/triggers.go61
-rw-r--r--config/ui.go261
-rw-r--r--config/viewer.go49
10 files changed, 465 insertions, 541 deletions
diff --git a/config/accounts.go b/config/accounts.go
index e9b7c633..8aadb329 100644
--- a/config/accounts.go
+++ b/config/accounts.go
@@ -10,7 +10,6 @@ import (
"reflect"
"regexp"
"sort"
- "strconv"
"strings"
"time"
@@ -70,30 +69,32 @@ func (c *RemoteConfig) ConnectionString() (string, error) {
}
type AccountConfig struct {
- Archive string `ini:"archive"`
- CopyTo string `ini:"copy-to"`
- Default string `ini:"default"`
- Postpone string `ini:"postpone"`
- From *mail.Address `ini:"-"`
- Aliases []*mail.Address `ini:"-"`
- Name string `ini:"-"`
- Source string `ini:"-"`
- Folders []string `ini:"folders" delim:","`
- FoldersExclude []string `ini:"folders-exclude" delim:","`
- Params map[string]string `ini:"-"`
- Outgoing RemoteConfig `ini:"-"`
- SignatureFile string `ini:"signature-file"`
- SignatureCmd string `ini:"signature-cmd"`
- EnableFoldersSort bool `ini:"enable-folders-sort"`
- FoldersSort []string `ini:"folders-sort" delim:","`
- AddressBookCmd string `ini:"address-book-cmd"`
- SendAsUTC bool `ini:"send-as-utc"`
- LocalizedRe *regexp.Regexp `ini:"-"`
+ Name string
+ // backend specific
+ Params map[string]string
+
+ Archive string `ini:"archive" default:"Archive"`
+ CopyTo string `ini:"copy-to"`
+ Default string `ini:"default" default:"INBOX"`
+ Postpone string `ini:"postpone" default:"Drafts"`
+ From *mail.Address `ini:"from"`
+ Aliases []*mail.Address `ini:"aliases"`
+ Source string `ini:"source" parse:"ParseSource"`
+ Folders []string `ini:"folders" delim:","`
+ FoldersExclude []string `ini:"folders-exclude" delim:","`
+ Outgoing RemoteConfig `ini:"outgoing" parse:"ParseOutgoing"`
+ SignatureFile string `ini:"signature-file"`
+ SignatureCmd string `ini:"signature-cmd"`
+ EnableFoldersSort bool `ini:"enable-folders-sort" default:"true"`
+ FoldersSort []string `ini:"folders-sort" delim:","`
+ AddressBookCmd string `ini:"address-book-cmd"`
+ SendAsUTC bool `ini:"send-as-utc" default:"false"`
+ LocalizedRe *regexp.Regexp `ini:"subject-re-pattern" default:"(?i)^((AW|RE|SV|VS|ODP|R): ?)+"`
// CheckMail
CheckMail time.Duration `ini:"check-mail"`
CheckMailCmd string `ini:"check-mail-cmd"`
- CheckMailTimeout time.Duration `ini:"check-mail-timeout"`
+ CheckMailTimeout time.Duration `ini:"check-mail-timeout" default:"10s"`
CheckMailInclude []string `ini:"check-mail-include"`
CheckMailExclude []string `ini:"check-mail-exclude"`
@@ -101,7 +102,7 @@ type AccountConfig struct {
PgpKeyId string `ini:"pgp-key-id"`
PgpAutoSign bool `ini:"pgp-auto-sign"`
PgpOpportunisticEncrypt bool `ini:"pgp-opportunistic-encrypt"`
- PgpErrorLevel int `ini:"-"`
+ PgpErrorLevel int `ini:"pgp-error-level" parse:"ParsePgpErrorLevel" default:"warn"`
// AuthRes
TrustedAuthRes []string `ini:"trusted-authres" delim:","`
@@ -130,7 +131,6 @@ func parseAccounts(root string, accts []string) error {
// No config triggers account configuration wizard
return nil
}
- file.NameMapper = mapName
for _, _sec := range file.SectionStrings() {
if _sec == "DEFAULT" {
@@ -140,88 +140,27 @@ func parseAccounts(root string, accts []string) error {
continue
}
sec := file.Section(_sec)
- sourceRemoteConfig := RemoteConfig{}
account := AccountConfig{
- Archive: "Archive",
- Default: "INBOX",
- Postpone: "Drafts",
- Name: _sec,
- Params: make(map[string]string),
- EnableFoldersSort: true,
- CheckMailTimeout: 10 * time.Second,
- PgpErrorLevel: PgpErrorLevelWarn,
- // localizedRe contains a list of known translations for the common Re:
- LocalizedRe: regexp.MustCompile(`(?i)^((AW|RE|SV|VS|ODP|R): ?)+`),
+ Name: _sec,
+ Params: make(map[string]string),
}
- if err = sec.MapTo(&account); err != nil {
+ if err = MapToStruct(sec, &account, true); err != nil {
return err
}
for key, val := range sec.KeysHash() {
- switch key {
- case "source":
- sourceRemoteConfig.Value = val
- case "source-cred-cmd":
- sourceRemoteConfig.PasswordCmd = val
- case "outgoing":
- account.Outgoing.Value = val
- case "outgoing-cred-cmd":
- account.Outgoing.PasswordCmd = val
- case "outgoing-cred-cmd-cache":
- cache, err := strconv.ParseBool(val)
- if err != nil {
- return fmt.Errorf("%s=%s %w", key, val, err)
- }
- account.Outgoing.CacheCmd = cache
- case "from":
- addr, err := mail.ParseAddress(val)
- if err != nil {
- return fmt.Errorf("%s=%s %w", key, val, err)
- }
- account.From = addr
- case "aliases":
- addrs, err := mail.ParseAddressList(val)
- if err != nil {
- return fmt.Errorf("%s=%s %w", key, val, err)
- }
- account.Aliases = addrs
- case "subject-re-pattern":
- re, err := regexp.Compile(val)
- if err != nil {
- return fmt.Errorf("%s=%s %w", key, val, err)
- }
- account.LocalizedRe = re
- case "pgp-error-level":
- switch strings.ToLower(val) {
- case "none":
- account.PgpErrorLevel = PgpErrorLevelNone
- case "warn":
- account.PgpErrorLevel = PgpErrorLevelWarn
- case "error":
- account.PgpErrorLevel = PgpErrorLevelError
- default:
- return fmt.Errorf("unknown pgp-error-level: %s", val)
- }
- default:
- backendSpecific := true
- typ := reflect.TypeOf(account)
- for i := 0; i < typ.NumField(); i++ {
- field := typ.Field(i)
- if field.Tag.Get("ini") == key {
- backendSpecific = false
- break
- }
- }
- if backendSpecific {
- account.Params[key] = val
+ backendSpecific := true
+ typ := reflect.TypeOf(account)
+ for i := 0; i < typ.NumField(); i++ {
+ field := typ.Field(i)
+ if field.Tag.Get("ini") == key {
+ backendSpecific = false
+ break
}
}
+ if backendSpecific {
+ account.Params[key] = val
+ }
}
- source, err := sourceRemoteConfig.ConnectionString()
- if err != nil {
- return fmt.Errorf("Invalid source credentials for %s: %w", _sec, err)
- }
- account.Source = source
-
if account.Source == "" {
return fmt.Errorf("Expected source for account %s", _sec)
}
@@ -229,11 +168,6 @@ func parseAccounts(root string, accts []string) error {
return fmt.Errorf("Expected from for account %s", _sec)
}
- _, err = account.Outgoing.parseValue()
- if err != nil {
- return fmt.Errorf("Invalid outgoing credentials for %s: %w", _sec, err)
- }
-
log.Debugf("accounts.conf: [%s] from = %s", account.Name, account.From)
Accounts = append(Accounts, &account)
}
@@ -251,6 +185,48 @@ func parseAccounts(root string, accts []string) error {
return nil
}
+func (a *AccountConfig) ParseSource(sec *ini.Section, key *ini.Key) (string, error) {
+ var remote RemoteConfig
+ remote.Value = key.String()
+ if k, err := sec.GetKey("source-cred-cmd"); err == nil {
+ remote.PasswordCmd = k.String()
+ }
+ return remote.ConnectionString()
+}
+
+func (a *AccountConfig) ParseOutgoing(sec *ini.Section, key *ini.Key) (RemoteConfig, error) {
+ var remote RemoteConfig
+ remote.Value = key.String()
+ if k, err := sec.GetKey("outgoing-cred-cmd"); err == nil {
+ remote.PasswordCmd = k.String()
+ }
+ if k, err := sec.GetKey("outgoing-cred-cmd-cache"); err == nil {
+ cache, err := k.Bool()
+ if err != nil {
+ return remote, err
+ }
+ remote.CacheCmd = cache
+ }
+ _, err := remote.parseValue()
+ return remote, err
+}
+
+func (a *AccountConfig) ParsePgpErrorLevel(sec *ini.Section, key *ini.Key) (int, error) {
+ var level int
+ var err error
+ switch strings.ToLower(key.String()) {
+ case "none":
+ level = PgpErrorLevelNone
+ case "warn":
+ level = PgpErrorLevelWarn
+ case "error":
+ level = PgpErrorLevelError
+ default:
+ err = fmt.Errorf("unknown level: %s", key.String())
+ }
+ return level, err
+}
+
// checkConfigPerms checks for too open permissions
// printing the fix on stdout and returning an error
func checkConfigPerms(filename string) error {
diff --git a/config/compose.go b/config/compose.go
index 14a3087b..70533453 100644
--- a/config/compose.go
+++ b/config/compose.go
@@ -1,7 +1,6 @@
package config
import (
- "fmt"
"regexp"
"git.sr.ht/~rjarry/aerc/log"
@@ -10,54 +9,29 @@ import (
type ComposeConfig struct {
Editor string `ini:"editor"`
- HeaderLayout [][]string `ini:"-"`
+ HeaderLayout [][]string `ini:"header-layout" parse:"ParseLayout" default:"To|From,Subject"`
AddressBookCmd string `ini:"address-book-cmd"`
- ReplyToSelf bool `ini:"reply-to-self"`
- NoAttachmentWarning *regexp.Regexp `ini:"-"`
+ ReplyToSelf bool `ini:"reply-to-self" default:"true"`
+ NoAttachmentWarning *regexp.Regexp `ini:"no-attachment-warning" parse:"ParseNoAttachmentWarning"`
FilePickerCmd string `ini:"file-picker-cmd"`
FormatFlowed bool `ini:"format-flowed"`
}
-func defaultComposeConfig() *ComposeConfig {
- return &ComposeConfig{
- HeaderLayout: [][]string{
- {"To", "From"},
- {"Subject"},
- },
- ReplyToSelf: true,
- }
-}
-
-var Compose = defaultComposeConfig()
+var Compose = new(ComposeConfig)
func parseCompose(file *ini.File) error {
- compose, err := file.GetSection("compose")
- if err != nil {
- goto end
- }
-
- if err := compose.MapTo(&Compose); err != nil {
+ if err := MapToStruct(file.Section("compose"), Compose, true); err != nil {
return err
}
- for key, val := range compose.KeysHash() {
- if key == "header-layout" {
- Compose.HeaderLayout = parseLayout(val)
- }
-
- if key == "no-attachment-warning" && len(val) > 0 {
- re, err := regexp.Compile("(?im)" + val)
- if err != nil {
- return fmt.Errorf(
- "Invalid no-attachment-warning '%s': %w",
- val, err,
- )
- }
-
- Compose.NoAttachmentWarning = re
- }
- }
-
-end:
log.Debugf("aerc.conf: [compose] %#v", Compose)
return nil
}
+
+func (c *ComposeConfig) ParseLayout(sec *ini.Section, key *ini.Key) ([][]string, error) {
+ layout := parseLayout(key.String())
+ return layout, nil
+}
+
+func (c *ComposeConfig) ParseNoAttachmentWarning(sec *ini.Section, key *ini.Key) (*regexp.Regexp, error) {
+ return regexp.Compile(`(?im)` + key.String())
+}
diff --git a/config/config.go b/config/config.go
index 6bf767af..de8cd0d0 100644
--- a/config/config.go
+++ b/config/config.go
@@ -7,28 +7,12 @@ import (
"os"
"path"
"strings"
- "unicode"
"github.com/go-ini/ini"
"github.com/kyoh86/xdg"
"github.com/mitchellh/go-homedir"
)
-// Input: TimestampFormat
-// Output: timestamp-format
-func mapName(raw string) string {
- newstr := make([]rune, 0, len(raw))
- for i, chr := range raw {
- if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
- if i > 0 {
- newstr = append(newstr, '-')
- }
- }
- newstr = append(newstr, unicode.ToLower(chr))
- }
- return string(newstr)
-}
-
// Set at build time
var (
shareDir string
@@ -126,7 +110,6 @@ func LoadConfigFromFile(root *string, accts []string) error {
if err != nil {
return err
}
- file.NameMapper = mapName
if err := parseGeneral(file); err != nil {
return err
diff --git a/config/general.go b/config/general.go
index 2921fdf5..0968976b 100644
--- a/config/general.go
+++ b/config/general.go
@@ -12,45 +12,20 @@ import (
type GeneralConfig struct {
DefaultSavePath string `ini:"default-save-path"`
- PgpProvider string `ini:"pgp-provider"`
+ PgpProvider string `ini:"pgp-provider" default:"auto" parse:"ParsePgpProvider"`
UnsafeAccountsConf bool `ini:"unsafe-accounts-conf"`
LogFile string `ini:"log-file"`
- LogLevel log.LogLevel `ini:"-"`
+ LogLevel log.LogLevel `ini:"log-level" default:"info" parse:"ParseLogLevel"`
}
-func defaultGeneralConfig() *GeneralConfig {
- return &GeneralConfig{
- PgpProvider: "auto",
- UnsafeAccountsConf: false,
- LogLevel: log.INFO,
- }
-}
-
-var General = defaultGeneralConfig()
+var General = new(GeneralConfig)
func parseGeneral(file *ini.File) error {
- var level *ini.Key
var logFile *os.File
- gen, err := file.GetSection("general")
- if err != nil {
- goto end
- }
- if err := gen.MapTo(&General); err != nil {
+ if err := MapToStruct(file.Section("general"), General, true); err != nil {
return err
}
- level, err = gen.GetKey("log-level")
- if err == nil {
- l, err := log.ParseLevel(level.String())
- if err != nil {
- return err
- }
- General.LogLevel = l
- }
- if err := General.validatePgpProvider(); err != nil {
- return err
- }
-end:
if !isatty.IsTerminal(os.Stdout.Fd()) {
logFile = os.Stdout
// redirected to file, force TRACE level
@@ -71,11 +46,14 @@ end:
return nil
}
-func (gen *GeneralConfig) validatePgpProvider() error {
- switch gen.PgpProvider {
+func (gen *GeneralConfig) ParseLogLevel(sec *ini.Section, key *ini.Key) (log.LogLevel, error) {
+ return log.ParseLevel(key.String())
+}
+
+func (gen *GeneralConfig) ParsePgpProvider(sec *ini.Section, key *ini.Key) (string, error) {
+ switch key.String() {
case "gpg", "internal", "auto":
- return nil
- default:
- return fmt.Errorf("pgp-provider must be either auto, gpg or internal")
+ return key.String(), nil
}
+ return "", fmt.Errorf("must be either auto, gpg or internal")
}
diff --git a/config/parse.go b/config/parse.go
new file mode 100644
index 00000000..d836c760
--- /dev/null
+++ b/config/parse.go
@@ -0,0 +1,233 @@
+package config
+
+import (
+ "errors"
+ "fmt"
+ "reflect"
+ "regexp"
+
+ "git.sr.ht/~rjarry/aerc/lib/templates"
+ "github.com/emersion/go-message/mail"
+ "github.com/go-ini/ini"
+)
+
+func MapToStruct(s *ini.Section, v interface{}, useDefaults bool) error {
+ typ := reflect.TypeOf(v)
+ val := reflect.ValueOf(v)
+ if typ.Kind() == reflect.Ptr {
+ typ = typ.Elem()
+ val = val.Elem()
+ } else {
+ panic("MapToStruct requires a pointer")
+ }
+ if typ.Kind() != reflect.Struct {
+ panic("MapToStruct requires a pointer to a struct")
+ }
+
+ for i := 0; i < typ.NumField(); i++ {
+ fieldVal := val.Field(i)
+ fieldType := typ.Field(i)
+
+ name := fieldType.Tag.Get("ini")
+ if name == "" || name == "-" {
+ continue
+ }
+ key, err := s.GetKey(name)
+ if err != nil {
+ defValue, found := fieldType.Tag.Lookup("default")
+ if useDefaults && found {
+ key, _ = s.NewKey(name, defValue)
+ } else {
+ continue
+ }
+ }
+ err = setField(s, key, reflect.ValueOf(v), fieldVal, fieldType)
+ if err != nil {
+ return fmt.Errorf("[%s].%s: %w", s.Name(), name, err)
+ }
+ }
+ return nil
+}
+
+func setField(
+ s *ini.Section, key *ini.Key, struc reflect.Value,
+ fieldVal reflect.Value, fieldType reflect.StructField,
+) error {
+ var methodValue reflect.Value
+ method := getParseMethod(s, key, struc, fieldType)
+ if method.IsValid() {
+ in := []reflect.Value{reflect.ValueOf(s), reflect.ValueOf(key)}
+ out := method.Call(in)
+ err, _ := out[1].Interface().(error)
+ if err != nil {
+ return err
+ }
+ methodValue = out[0]
+ }
+
+ ft := fieldType.Type
+
+ switch ft.Kind() {
+ case reflect.String:
+ if method.IsValid() {
+ fieldVal.SetString(methodValue.String())
+ } else {
+ fieldVal.SetString(key.String())
+ }
+ case reflect.Bool:
+ if method.IsValid() {
+ fieldVal.SetBool(methodValue.Bool())
+ } else {
+ boolVal, err := key.Bool()
+ if err != nil {
+ return err
+ }
+ fieldVal.SetBool(boolVal)
+ }
+ case reflect.Int32:
+ // impossible to differentiate rune from int32, they are aliases
+ // this is an ugly hack but there is no alternative...
+ if fieldType.Tag.Get("type") == "rune" {
+ if method.IsValid() {
+ fieldVal.Set(methodValue)
+ } else {
+ runes := []rune(key.String())
+ if len(runes) != 1 {
+ return errors.New("value must be 1 character long")
+ }
+ fieldVal.Set(reflect.ValueOf(runes[0]))
+ }
+ return nil
+ }
+ fallthrough
+ case reflect.Int64:
+ // ParseDuration will not return err for `0`, so check the type name
+ if ft.PkgPath() == "time" && ft.Name() == "Duration" {
+ durationVal, err := key.Duration()
+ if err != nil {
+ return err
+ }
+ fieldVal.Set(reflect.ValueOf(durationVal))
+ return nil
+ }
+ fallthrough
+ case reflect.Int, reflect.Int8, reflect.Int16:
+ if method.IsValid() {
+ fieldVal.SetInt(methodValue.Int())
+ } else {
+ intVal, err := key.Int64()
+ if err != nil {
+ return err
+ }
+ fieldVal.SetInt(intVal)
+ }
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ if method.IsValid() {
+ fieldVal.SetUint(methodValue.Uint())
+ } else {
+ uintVal, err := key.Uint64()
+ if err != nil {
+ return err
+ }
+ fieldVal.SetUint(uintVal)
+ }
+ case reflect.Float32, reflect.Float64:
+ if method.IsValid() {
+ fieldVal.SetFloat(methodValue.Float())
+ } else {
+ floatVal, err := key.Float64()
+ if err != nil {
+ return err
+ }
+ fieldVal.SetFloat(floatVal)
+ }
+ case reflect.Slice, reflect.Array:
+ switch {
+ case method.IsValid():
+ fieldVal.Set(methodValue)
+ case ft.Elem().Kind() == reflect.Ptr &&
+ typePath(ft.Elem().Elem()) == "net/mail.Address":
+ addrs, err := mail.ParseAddressList(key.String())
+ if err != nil {
+ return err
+ }
+ fieldVal.Set(reflect.ValueOf(addrs))
+ case ft.Elem().Kind() == reflect.String:
+ delim := fieldType.Tag.Get("delim")
+ fieldVal.Set(reflect.ValueOf(key.Strings(delim)))
+ default:
+ panic(fmt.Sprintf("unsupported type []%s", typePath(ft.Elem())))
+ }
+ case reflect.Struct:
+ if method.IsValid() {
+ fieldVal.Set(methodValue)
+ } else {
+ panic(fmt.Sprintf("unsupported type %s", typePath(ft)))
+ }
+ case reflect.Ptr:
+ if method.IsValid() {
+ fieldVal.Set(methodValue)
+ } else {
+ switch typePath(ft.Elem()) {
+ case "net/mail.Address":
+ addr, err := mail.ParseAddress(key.String())
+ if err != nil {
+ return err
+ }
+ fieldVal.Set(reflect.ValueOf(addr))
+ case "regexp.Regexp":
+ r, err := regexp.Compile(key.String())
+ if err != nil {
+ return err
+ }
+ fieldVal.Set(reflect.ValueOf(r))
+ case "text/template.Template":
+ t, err := templates.ParseTemplate(key.String(), key.String())
+ if err != nil {
+ return err
+ }
+ fieldVal.Set(reflect.ValueOf(t))
+ default:
+ panic(fmt.Sprintf("unsupported type %s", typePath(ft)))
+ }
+ }
+ default:
+ panic(fmt.Sprintf("unsupported type %s", typePath(ft)))
+ }
+ return nil
+}
+
+func getParseMethod(
+ section *ini.Section, key *ini.Key,
+ struc reflect.Value, typ reflect.StructField,
+) reflect.Value {
+ methodName, found := typ.Tag.Lookup("parse")
+ if !found {
+ return reflect.Value{}
+ }
+ method := struc.MethodByName(methodName)
+ if !method.IsValid() {
+ panic(fmt.Sprintf("(*%s).%s: method not found",
+ struc, methodName))
+ }
+
+ if method.Type().NumIn() != 2 ||
+ method.Type().In(0) != reflect.TypeOf(section) ||
+ method.Type().In(1) != reflect.TypeOf(key) ||
+ method.Type().NumOut() != 2 {
+ panic(fmt.Sprintf("(*%s).%s: invalid signature, expected %s",
+ struc.Elem().Type().Name(), methodName,
+ "func(*ini.Section, *ini.Key) (any, error)"))
+ }
+
+ return method
+}
+
+func typePath(t reflect.Type) string {
+ var prefix string
+ if t.Kind() == reflect.Ptr {
+ t = t.Elem()
+ prefix = "*"
+ }
+ return fmt.Sprintf("%s%s.%s", prefix, t.PkgPath(), t.Name())
+}
diff --git a/config/statusline.go b/config/statusline.go
index 7cc2140c..d09b8fa3 100644
--- a/config/statusline.go
+++ b/config/statusline.go
@@ -10,63 +10,22 @@ import (
)
type StatuslineConfig struct {
- StatusColumns []*ColumnDef `ini:"-"`
- ColumnSeparator string `ini:"column-separator"`
- Separator string `ini:"separator"`
- DisplayMode string `ini:"display-mode"`
- // deprecated
- RenderFormat string `ini:"render-format"`
+ StatusColumns []*ColumnDef `ini:"status-columns" parse:"ParseColumns" default:"left<*,center>=,right>*"`
+ ColumnSeparator string `ini:"column-separator" default:" "`
+ Separator string `ini:"separator" default:" | "`
+ DisplayMode string `ini:"display-mode" default:"text"`
}
-func defaultStatuslineConfig() *StatuslineConfig {
- left, _ := templates.ParseTemplate("column-left", `[{{.Account}}] {{.StatusInfo}}`)
- center, _ := templates.ParseTemplate("column-center", `{{.PendingKeys}}`)
- right, _ := templates.ParseTemplate("column-right", `{{.TrayInfo}}`)
- return &StatuslineConfig{
- StatusColumns: []*ColumnDef{
- {
- Name: "left",
- Template: left,
- Flags: ALIGN_LEFT | WIDTH_AUTO,
- },
- {
- Name: "center",
- Template: center,
- Flags: ALIGN_CENTER | WIDTH_FIT,
- },
- {
- Name: "right",
- Template: right,
- Flags: ALIGN_RIGHT | WIDTH_AUTO,
- },
- },
- ColumnSeparator: " ",
- Separator: " | ",
- DisplayMode: "text",
- // deprecated
- RenderFormat: "",
- }
-}
-
-var Statusline = defaultStatuslineConfig()
+var Statusline = new(StatuslineConfig)
func parseStatusline(file *ini.File) error {
- statusline, err := file.GetSection("statusline")
- if err != nil {
- goto out
- }
- if err := statusline.MapTo(&Statusline); err != nil {
+ statusline := file.Section("statusline")
+ if err := MapToStruct(statusline, Statusline, true); err != nil {
return err
}
- if key, err := statusline.GetKey("status-columns"); err == nil {
- columns, err := ParseColumnDefs(key, statusline)
- if err != nil {
- return err
- }
- Statusline.StatusColumns = columns
- } else if Statusline.RenderFormat != "" {
- columns, err := convertRenderFormat()
+ if key, err := statusline.GetKey("render-format"); err == nil {
+ columns, err := convertRenderFormat(key.String())
if err != nil {
return err
}
@@ -92,20 +51,32 @@ index-format will be removed in aerc 0.17.
})
}
-out:
log.Debugf("aerc.conf: [statusline] %#v", Statusline)
return nil
}
+func (s *StatuslineConfig) ParseColumns(sec *ini.Section, key *ini.Key) ([]*ColumnDef, error) {
+ if !sec.HasKey("column-left") {
+ _, _ = sec.NewKey("column-left", "[{{.Account}}] {{.StatusInfo}}")
+ }
+ if !sec.HasKey("column-center") {
+ _, _ = sec.NewKey("column-center", "{{.PendingKeys}}")
+ }
+ if !sec.HasKey("column-right") {
+ _, _ = sec.NewKey("column-right", "{{.TrayInfo}}")
+ }
+ return ParseColumnDefs(key, sec)
+}
+
var (
renderFmtRe = regexp.MustCompile(`%(-?\d+)?(\.\d+)?[acdmSTp]`)
statuslineMute = false
)
-func convertRenderFormat() ([]*ColumnDef, error) {
+func convertRenderFormat(renderFormat string) ([]*ColumnDef, error) {
var columns []*ColumnDef
- tokens := strings.Split(Statusline.RenderFormat, "%>")
+ tokens := strings.Split(renderFormat, "%>")
left := renderFmtRe.ReplaceAllStringFunc(
tokens[0], renderVerbToTemplate)
diff --git a/config/templates.go b/config/templates.go
index f618d365..d3e7cfb0 100644
--- a/config/templates.go
+++ b/config/templates.go
@@ -2,7 +2,6 @@ package config
import (
"path"
- "strings"
"time"
"git.sr.ht/~rjarry/aerc/lib/templates"
@@ -13,31 +12,16 @@ import (
type TemplateConfig struct {
TemplateDirs []string `ini:"template-dirs" delim:":"`
- NewMessage string `ini:"new-message"`
- QuotedReply string `ini:"quoted-reply"`
- Forwards string `ini:"forwards"`
+ NewMessage string `ini:"new-message" default:"new_message"`
+ QuotedReply string `ini:"quoted-reply" default:"quoted_reply"`
+ Forwards string `ini:"forwards" default:"forward_as_body"`
}
-func defaultTemplatesConfig() *TemplateConfig {
- return &TemplateConfig{
- TemplateDirs: []string{},
- NewMessage: "new_message",
- QuotedReply: "quoted_reply",
- Forwards: "forward_as_body",
- }
-}
-
-var Templates = defaultTemplatesConfig()
+var Templates = new(TemplateConfig)
func parseTemplates(file *ini.File) error {
- if templatesSec, err := file.GetSection("templates"); err == nil {
- if err := templatesSec.MapTo(&Templates); err != nil {
- return err
- }
- templateDirs := templatesSec.Key("template-dirs").String()
- if templateDirs != "" {
- Templates.TemplateDirs = strings.Split(templateDirs, ":")
- }
+ if err := MapToStruct(file.Section("templates"), Templates, true); err != nil {
+ return err
}
// append default paths to template-dirs
diff --git a/config/triggers.go b/config/triggers.go
index c0e70d40..82750c2d 100644
--- a/config/triggers.go
+++ b/config/triggers.go
@@ -9,43 +9,45 @@ import (
)
type TriggersConfig struct {
- NewEmail []string `ini:"-"`
+ NewEmail []string `ini:"new-email" parse:"ParseNewEmail"`
}
-var Triggers = &TriggersConfig{}
+var Triggers = new(TriggersConfig)
func parseTriggers(file *ini.File) error {
- var cmd string
- triggers, err := file.GetSection("triggers")
+ if err := MapToStruct(file.Section("triggers"), Triggers, true); err != nil {
+ return err
+ }
+ log.Debugf("aerc.conf: [triggers] %#v", Triggers)
+ return nil
+}
+
+func (t *TriggersConfig) ParseNewEmail(_ *ini.Section, key *ini.Key) ([]string, error) {
+ cmd := indexFmtRegexp.ReplaceAllStringFunc(
+ key.String(),
+ func(s string) string {
+ runes := []rune(s)
+ t, _ := indexVerbToTemplate(runes[len(runes)-1])
+ return t
+ },
+ )
+ args, err := shlex.Split(cmd)
if err != nil {
- goto out
+ return nil, err
}
- if key := triggers.Key("new-email"); key != nil {
- cmd = indexFmtRegexp.ReplaceAllStringFunc(
- key.String(),
- func(s string) string {
- runes := []rune(s)
- t, _ := indexVerbToTemplate(runes[len(runes)-1])
- return t
- },
- )
- Triggers.NewEmail, err = shlex.Split(cmd)
- if err != nil {
- return err
- }
- if cmd != key.String() {
- log.Warnf("%s %s",
- "The new-email trigger now uses templates instead of %-based placeholders.",
- "Backward compatibility will be removed in aerc 0.17.")
- Warnings = append(Warnings, Warning{
- Title: "FORMAT CHANGED: [triggers].new-email",
- Body: `
+ if cmd != key.String() {
+ log.Warnf("%s %s",
+ "The new-email trigger now uses templates instead of %-based placeholders.",
+ "Backward compatibility will be removed in aerc 0.17.")
+ Warnings = append(Warnings, Warning{
+ Title: "FORMAT CHANGED: [triggers].new-email",
+ Body: `
The new-email trigger now uses templates instead of %-based placeholders.
Your configuration in this instance was automatically converted to:
[triggers]
-new-email = ` + format.ShellQuote(Triggers.NewEmail) + `
+new-email = ` + format.ShellQuote(args) + `
Your configuration file was not changed. To make this change permanent and to
dismiss this warning on launch, replace the above line into aerc.conf. See
@@ -53,10 +55,7 @@ aerc-config(5) for more details.
The automatic conversion of new-email will be removed in aerc 0.17.
`,
- })
- }
+ })
}
-out:
- log.Debugf("aerc.conf: [triggers] %#v", Triggers)
- return nil
+ return args, nil
}
diff --git a/config/ui.go b/config/ui.go
index 64469643..f52cf7f8 100644
--- a/config/ui.go
+++ b/config/ui.go
@@ -17,17 +17,14 @@ import (
)
type UIConfig struct {
- IndexColumns []*ColumnDef `ini:"-"`
- ColumnSeparator string `ini:"column-separator"`
- // deprecated
- IndexFormat string `ini:"index-format"`
+ IndexColumns []*ColumnDef `ini:"index-columns" parse:"ParseIndexColumns" default:"date<20,name<17,flags>4,subject<*"`
+ ColumnSeparator string `ini:"column-separator" default:" "`
- DirListFormat string `ini:"dirlist-format"` // deprecated
- DirListLeft *template.Template `ini:"-"`
- DirListRight *template.Template `ini:"-"`
+ DirListLeft *template.Template `ini:"dirlist-left" default:"{{.Folder}}"`
+ DirListRight *template.Template `ini:"dirlist-right" default:"{{if .Unread}}{{humanReadable .Unread}}/{{end}}{{if .Exists}}{{humanReadable .Exists}}{{end}}"`
- AutoMarkRead bool `ini:"auto-mark-read"`
- TimestampFormat string `ini:"timestamp-format"`
+ AutoMarkRead bool `ini:"auto-mark-read" default:"true"`
+ TimestampFormat string `ini:"timestamp-format" default:"2006-01-02 03:04 PM"`
ThisDayTimeFormat string `ini:"this-day-time-format"`
ThisWeekTimeFormat string `ini:"this-week-time-format"`
ThisYearTimeFormat string `ini:"this-year-time-format"`
@@ -35,49 +32,48 @@ type UIConfig struct {
MessageViewThisDayTimeFormat string `ini:"message-view-this-day-time-format"`
MessageViewThisWeekTimeFormat string `ini:"message-view-this-week-time-format"`
MessageViewThisYearTimeFormat string `ini:"message-view-this-year-time-format"`
- PinnedTabMarker string `ini:"pinned-tab-marker"`
- SidebarWidth int `ini:"sidebar-width"`
- PreviewHeight int `ini:"preview-height"`
- EmptyMessage string `ini:"empty-message"`
- EmptyDirlist string `ini:"empty-dirlist"`
+ PinnedTabMarker string "ini:\"pinned-tab-marker\" default:\"`\""
+ SidebarWidth int `ini:"sidebar-width" default:"20"`
+ EmptyMessage string `ini:"empty-message" default:"(no messages)"`
+ EmptyDirlist string `ini:"empty-dirlist" default:"(no folders)"`
MouseEnabled bool `ini:"mouse-enabled"`
ThreadingEnabled bool `ini:"threading-enabled"`
ForceClientThreads bool `ini:"force-client-threads"`
- ClientThreadsDelay time.Duration `ini:"client-threads-delay"`
+ ClientThreadsDelay time.Duration `ini:"client-threads-delay" default:"50ms"`
FuzzyComplete bool `ini:"fuzzy-complete"`
- NewMessageBell bool `ini:"new-message-bell"`
- Spinner string `ini:"spinner"`
- SpinnerDelimiter string `ini:"spinner-delimiter"`
- SpinnerInterval time.Duration `ini:"spinner-interval"`
+ NewMessageBell bool `ini:"new-message-bell" default:"true"`
+ Spinner string `ini:"spinner" default:"[..] , [..] , [..] , [..] , [..], [..] , [..] , [..] "`
+ SpinnerDelimiter string `ini:"spinner-delimiter" default:","`
+ SpinnerInterval time.Duration `ini:"spinner-interval" default:"200ms"`
IconUnencrypted string `ini:"icon-unencrypted"`
- IconEncrypted string `ini:"icon-encrypted"`
- IconSigned string `ini:"icon-signed"`
+ IconEncrypted string `ini:"icon-encrypted" default:"[e]"`
+ IconSigned string `ini:"icon-signed" default:"[s]"`
IconSignedEncrypted string `ini:"icon-signed-encrypted"`
- IconUnknown string `ini:"icon-unknown"`
- IconInvalid string `ini:"icon-invalid"`
- IconAttachment string `ini:"icon-attachment"`
- DirListDelay time.Duration `ini:"dirlist-delay"`
+ IconUnknown string `ini:"icon-unknown" default:"[s?]"`
+ IconInvalid string `ini:"icon-invalid" default:"[s!]"`
+ IconAttachment string `ini:"icon-attachment" default:"a"`
+ DirListDelay time.Duration `ini:"dirlist-delay" default:"200ms"`
DirListTree bool `ini:"dirlist-tree"`
DirListCollapse int `ini:"dirlist-collapse"`
- Sort []string `delim:" "`
+ Sort []string `ini:"sort" delim:" "`
NextMessageOnDelete bool `ini:"next-message-on-delete"`
- CompletionDelay time.Duration `ini:"completion-delay"`
- CompletionMinChars int `ini:"completion-min-chars"`
- CompletionPopovers bool `ini:"completion-popovers"`
+ CompletionDelay time.Duration `ini:"completion-delay" default:"250ms"`
+ CompletionMinChars int `ini:"completion-min-chars" default:"1"`
+ CompletionPopovers bool `ini:"completion-popovers" default:"true"`
StyleSetDirs []string `ini:"stylesets-dirs" delim:":"`
- StyleSetName string `ini:"styleset-name"`
+ StyleSetName string `ini:"styleset-name" default:"default"`
style StyleSet
// customize border appearance
- BorderCharVertical rune `ini:"-"`
- BorderCharHorizontal rune `ini:"-"`
+ BorderCharVertical rune `ini:"border-char-vertical" default:" " type:"rune"`
+ BorderCharHorizontal rune `ini:"border-char-horizontal" default:" " type:"rune"`
ReverseOrder bool `ini:"reverse-msglist-order"`
ReverseThreadOrder bool `ini:"reverse-thread-order"`
SortThreadSiblings bool `ini:"sort-thread-siblings"`
// Tab Templates
- TabTitleAccount *template.Template `ini:"-"`
- TabTitleComposer *template.Template `ini:"-"`
+ TabTitleAccount *template.Template `ini:"tab-title-account" default:"{{.Account}}"`
+ TabTitleComposer *template.Template `ini:"tab-title-composer" default:"{{.Subject}}"`
// private
contextualUis []*UiConfigContext
@@ -106,95 +102,14 @@ type uiContextKey struct {
const unreadExists string = `{{if .Unread}}{{humanReadable .Unread}}/{{end}}{{if .Exists}}{{humanReadable .Exists}}{{end}}`
-func defaultUiConfig() *UIConfig {
- date, _ := templates.ParseTemplate("column-date", "{{.DateAutoFormat .Date.Local}}")
- name, _ := templates.ParseTemplate("column-name", "{{index (.From | names) 0}}")
- flags, _ := templates.ParseTemplate("column-flags", `{{.Flags | join ""}}`)
- subject, _ := templates.ParseTemplate("column-subject", "{{.Subject}}")
- left, _ := templates.ParseTemplate("folder", "{{.Folder}}")
- right, _ := templates.ParseTemplate("ue", unreadExists)
- tabTitleAccount, _ := templates.ParseTemplate("tab-title-account", "{{.Account}}")
- tabTitleComposer, _ := templates.ParseTemplate("tab-title-composer", "{{.Subject}}")
- return &UIConfig{
- IndexFormat: "", // deprecated
- DirListFormat: "", // deprecated
- IndexColumns: []*ColumnDef{
- {
- Name: "date",
- Width: 20,
- Flags: ALIGN_LEFT | WIDTH_EXACT,
- Template: date,
- },
- {
- Name: "name",
- Width: 17,
- Flags: ALIGN_LEFT | WIDTH_EXACT,
- Template: name,
- },
- {
- Name: "flags",
- Width: 4,
- Flags: ALIGN_RIGHT | WIDTH_EXACT,
- Template: flags,
- },
- {
- Name: "subject",
- Flags: ALIGN_LEFT | WIDTH_AUTO,
- Template: subject,
- },
- },
- DirListLeft: left,
- DirListRight: right,
- ColumnSeparator: " ",
- AutoMarkRead: true,
- TimestampFormat: "2006-01-02 03:04 PM",
- ThisDayTimeFormat: "",
- ThisWeekTimeFormat: "",
- ThisYearTimeFormat: "",
- PinnedTabMarker: "`",
- SidebarWidth: 20,
- PreviewHeight: 12,
- EmptyMessage: "(no messages)",
- EmptyDirlist: "(no folders)",
- MouseEnabled: false,
- ClientThreadsDelay: 50 * time.Millisecond,
- NewMessageBell: true,
- TabTitleAccount: tabTitleAccount,
- TabTitleComposer: tabTitleComposer,
- FuzzyComplete: false,
- Spinner: "[..] , [..] , [..] , [..] , [..], [..] , [..] , [..] ",
- SpinnerDelimiter: ",",
- SpinnerInterval: 200 * time.Millisecond,
- IconUnencrypted: "",
- IconSigned: "[s]",
- IconEncrypted: "[e]",
- IconSignedEncrypted: "",
- IconUnknown: "[s?]",
- IconInvalid: "[s!]",
- IconAttachment: "a",
- DirListDelay: 200 * time.Millisecond,
- NextMessageOnDelete: true,
- CompletionDelay: 250 * time.Millisecond,
- CompletionMinChars: 1,
- CompletionPopovers: true,
- StyleSetDirs: []string{},
- StyleSetName: "default",
- // border defaults
- BorderCharVertical: ' ',
- BorderCharHorizontal: ' ',
- // private
- contextualCache: make(map[uiContextKey]*UIConfig),
- contextualCounts: make(map[uiContextType]int),
- }
+var Ui = &UIConfig{
+ contextualCounts: make(map[uiContextType]int),
+ contextualCache: make(map[uiContextKey]*UIConfig),
}
-var Ui = defaultUiConfig()
-
func parseUi(file *ini.File) error {
- if ui, err := file.GetSection("ui"); err == nil {
- if err := Ui.parse(ui); err != nil {
- return err
- }
+ if err := Ui.parse(file.Section("ui")); err != nil {
+ return err
}
for _, sectionName := range file.SectionStrings() {
@@ -283,64 +198,16 @@ func parseUi(file *ini.File) error {
}
func (config *UIConfig) parse(section *ini.Section) error {
- if err := section.MapTo(config); err != nil {
+ if err := MapToStruct(section, config, section.Name() == "ui"); err != nil {
return err
}
- if key, err := section.GetKey("border-char-vertical"); err == nil {
- chars := []rune(key.String())
- if len(chars) != 1 {
- return fmt.Errorf("%v must be one and only one character", key)
- }
- config.BorderCharVertical = chars[0]
- }
- if key, err := section.GetKey("border-char-horizontal"); err == nil {
- chars := []rune(key.String())
- if len(chars) != 1 {
- return fmt.Errorf("%v must be one and only one character", key)
- }
- config.BorderCharHorizontal = chars[0]
- }
-
- // Values with type=time.Duration must be explicitly set. If these
- // values are given a default in the struct passed to ui.MapTo, which
- // they are, a zero-value in the config won't overwrite the default.
- if key, err := section.GetKey("dirlist-delay"); err == nil {
- dur, err := key.Duration()
- if err != nil {
- return err
- }
- config.DirListDelay = dur
- }
- if key, err := section.GetKey("completion-delay"); err == nil {
- dur, err := key.Duration()
- if err != nil {
- return err
- }
- config.CompletionDelay = dur
- }
-
if config.MessageViewTimestampFormat == "" {
config.MessageViewTimestampFormat = config.TimestampFormat
}
- if config.MessageViewThisDayTimeFormat == "" {
- config.MessageViewThisDayTimeFormat = config.TimestampFormat
- }
- if config.MessageViewThisWeekTimeFormat == "" {
- config.MessageViewThisWeekTimeFormat = config.TimestampFormat
- }
- if config.MessageViewThisDayTimeFormat == "" {
- config.MessageViewThisDayTimeFormat = config.TimestampFormat
- }
- if key, err := section.GetKey("index-columns"); err == nil {
- columns, err := ParseColumnDefs(key, section)
- if err != nil {
- return err
- }
- config.IndexColumns = columns
- } else if config.IndexFormat != "" {
- columns, err := convertIndexFormat(config.IndexFormat)
+ if key, err := section.GetKey("index-format"); err == nil {
+ columns, err := convertIndexFormat(key.String())
if err != nil {
return err
}
@@ -366,24 +233,8 @@ index-format will be removed in aerc 0.17.
}
Warnings = append(Warnings, w)
}
- left, _ := section.GetKey("dirlist-left")
- if left != nil {
- t, err := templates.ParseTemplate(left.String(), left.String())
- if err != nil {
- return err
- }
- config.DirListLeft = t
- }
- right, _ := section.GetKey("dirlist-right")
- if right != nil {
- t, err := templates.ParseTemplate(right.String(), right.String())
- if err != nil {
- return err
- }
- config.DirListRight = t
- }
- if left == nil && right == nil && config.DirListFormat != "" {
- left, right := convertDirlistFormat(config.DirListFormat)
+ if key, err := section.GetKey("dirlist-format"); err == nil {
+ left, right := convertDirlistFormat(key.String())
l, err := templates.ParseTemplate(left, left)
if err != nil {
return err
@@ -418,26 +269,26 @@ dirlist-format will be removed in aerc 0.17.
}
Warnings = append(Warnings, w)
}
- if key, err := section.GetKey("tab-title-account"); err == nil {
- val := key.Value()
- tmpl, err := templates.ParseTemplate("tab-title-account", val)
- if err != nil {
- return err
- }
- config.TabTitleAccount = tmpl
- }
- if key, err := section.GetKey("tab-title-composer"); err == nil {
- val := key.Value()
- tmpl, err := templates.ParseTemplate("tab-title-composer", val)
- if err != nil {
- return err
- }
- config.TabTitleComposer = tmpl
- }
return nil
}
+func (*UIConfig) ParseIndexColumns(section *ini.Section, key *ini.Key) ([]*ColumnDef, error) {
+ if !section.HasKey("column-date") {
+ _, _ = section.NewKey("column-date", `{{.DateAutoFormat .Date.Local}}`)
+ }
+ if !section.HasKey("column-name") {
+ _, _ = section.NewKey("column-name", `{{index (.From | names) 0}}`)
+ }
+ if !section.HasKey("column-flags") {
+ _, _ = section.NewKey("column-flags", `{{.Flags | join ""}}`)
+ }
+ if !section.HasKey("column-subject") {
+ _, _ = section.NewKey("column-subject", `{{.Subject}}`)
+ }
+ return ParseColumnDefs(key, section)
+}
+
var indexFmtRegexp = regexp.MustCompile(`%(-?\d+)?(\.\d+)?([ACDFRTZadfgilnrstuv])`)
func convertIndexFormat(indexFormat string) ([]*ColumnDef, error) {
diff --git a/config/viewer.go b/config/viewer.go
index c359f43b..091fb4b6 100644
--- a/config/viewer.go
+++ b/config/viewer.go
@@ -1,56 +1,31 @@
package config
import (
- "strings"
-
"git.sr.ht/~rjarry/aerc/log"
"github.com/go-ini/ini"
)
type ViewerConfig struct {
- Pager string
- Alternatives []string
+ Pager string `ini:"pager" default:"less -R"`
+ Alternatives []string `ini:"alternatives" default:"text/plain,text/html" delim:","`
ShowHeaders bool `ini:"show-headers"`
AlwaysShowMime bool `ini:"always-show-mime"`
- ParseHttpLinks bool `ini:"parse-http-links"`
- HeaderLayout [][]string `ini:"-"`
- KeyPassthrough bool `ini:"-"`
-}
-
-func defaultViewerConfig() *ViewerConfig {
- return &ViewerConfig{
- Pager: "less -R",
- Alternatives: []string{"text/plain", "text/html"},
- ShowHeaders: false,
- HeaderLayout: [][]string{
- {"From", "To"},
- {"Cc", "Bcc"},
- {"Date"},
- {"Subject"},
- },
- ParseHttpLinks: true,
- }
+ ParseHttpLinks bool `ini:"parse-http-links" default:"true"`
+ HeaderLayout [][]string `ini:"header-layout" parse:"ParseLayout" default:"From|To,Cc|Bcc,Date,Subject"`
+ KeyPassthrough bool
}
-var Viewer = defaultViewerConfig()
+var Viewer = new(ViewerConfig)
func parseViewer(file *ini.File) error {
- viewer, err := file.GetSection("viewer")
- if err != nil {
- goto out
- }
- if err := viewer.MapTo(&Viewer); err != nil {
+ if err := MapToStruct(file.Section("viewer"), Viewer, true); err != nil {
return err
}
- for key, val := range viewer.KeysHash() {
- switch key {
- case "alternatives":
- Viewer.Alternatives = strings.Split(val, ",")
- case "header-layout":
- Viewer.HeaderLayout = parseLayout(val)
- }
- }
-out:
log.Debugf("aerc.conf: [viewer] %#v", Viewer)
return nil
}
+
+func (v *ViewerConfig) ParseLayout(sec *ini.Section, key *ini.Key) ([][]string, error) {
+ layout := parseLayout(key.String())
+ return layout, nil
+}