diff options
Diffstat (limited to 'config/ui.go')
-rw-r--r-- | config/ui.go | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/config/ui.go b/config/ui.go new file mode 100644 index 00000000..16a296df --- /dev/null +++ b/config/ui.go @@ -0,0 +1,340 @@ +package config + +import ( + "fmt" + "path" + "regexp" + "strings" + "time" + + "git.sr.ht/~rjarry/aerc/logging" + "github.com/gdamore/tcell/v2" + "github.com/go-ini/ini" + "github.com/imdario/mergo" +) + +type UIConfig struct { + AutoMarkRead bool `ini:"auto-mark-read"` + IndexFormat string `ini:"index-format"` + TimestampFormat string `ini:"timestamp-format"` + ThisDayTimeFormat string `ini:"this-day-time-format"` + ThisWeekTimeFormat string `ini:"this-week-time-format"` + ThisYearTimeFormat string `ini:"this-year-time-format"` + MessageViewTimestampFormat string `ini:"message-view-timestamp-format"` + 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"` + ShowHeaders []string `delim:","` + RenderAccountTabs string `ini:"render-account-tabs"` + 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"` + MouseEnabled bool `ini:"mouse-enabled"` + ThreadingEnabled bool `ini:"threading-enabled"` + ForceClientThreads bool `ini:"force-client-threads"` + ClientThreadsDelay time.Duration `ini:"client-threads-delay"` + FuzzyComplete bool `ini:"fuzzy-complete"` + NewMessageBell bool `ini:"new-message-bell"` + Spinner string `ini:"spinner"` + SpinnerDelimiter string `ini:"spinner-delimiter"` + IconUnencrypted string `ini:"icon-unencrypted"` + IconEncrypted string `ini:"icon-encrypted"` + IconSigned string `ini:"icon-signed"` + IconSignedEncrypted string `ini:"icon-signed-encrypted"` + IconUnknown string `ini:"icon-unknown"` + IconInvalid string `ini:"icon-invalid"` + DirListFormat string `ini:"dirlist-format"` + DirListDelay time.Duration `ini:"dirlist-delay"` + DirListTree bool `ini:"dirlist-tree"` + DirListCollapse int `ini:"dirlist-collapse"` + Sort []string `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"` + StyleSetDirs []string `ini:"stylesets-dirs" delim:":"` + StyleSetName string `ini:"styleset-name"` + style StyleSet + // customize border appearance + BorderCharVertical rune `ini:"-"` + BorderCharHorizontal rune `ini:"-"` + + ReverseOrder bool `ini:"reverse-msglist-order"` + ReverseThreadOrder bool `ini:"reverse-thread-order"` + SortThreadSiblings bool `ini:"sort-thread-siblings"` +} + +type ContextType int + +const ( + UI_CONTEXT_FOLDER ContextType = iota + UI_CONTEXT_ACCOUNT + UI_CONTEXT_SUBJECT + BIND_CONTEXT_ACCOUNT + BIND_CONTEXT_FOLDER +) + +type UIConfigContext struct { + ContextType ContextType + Regex *regexp.Regexp + UiConfig UIConfig +} + +func defaultUiConfig() UIConfig { + return UIConfig{ + AutoMarkRead: true, + IndexFormat: "%-20.20D %-17.17n %Z %s", + TimestampFormat: "2006-01-02 03:04 PM", + ThisDayTimeFormat: "", + ThisWeekTimeFormat: "", + ThisYearTimeFormat: "", + ShowHeaders: []string{ + "From", "To", "Cc", "Bcc", "Subject", "Date", + }, + RenderAccountTabs: "auto", + PinnedTabMarker: "`", + SidebarWidth: 20, + PreviewHeight: 12, + EmptyMessage: "(no messages)", + EmptyDirlist: "(no folders)", + MouseEnabled: false, + ClientThreadsDelay: 50 * time.Millisecond, + NewMessageBell: true, + FuzzyComplete: false, + Spinner: "[..] , [..] , [..] , [..] , [..], [..] , [..] , [..] ", + SpinnerDelimiter: ",", + IconUnencrypted: "", + IconSigned: "[s]", + IconEncrypted: "[e]", + IconSignedEncrypted: "", + IconUnknown: "[s?]", + IconInvalid: "[s!]", + DirListFormat: "%n %>r", + DirListDelay: 200 * time.Millisecond, + NextMessageOnDelete: true, + CompletionDelay: 250 * time.Millisecond, + CompletionMinChars: 1, + CompletionPopovers: true, + StyleSetDirs: []string{}, + StyleSetName: "default", + // border defaults + BorderCharVertical: ' ', + BorderCharHorizontal: ' ', + } +} + +func (config *AercConfig) parseUi(file *ini.File) error { + if ui, err := file.GetSection("ui"); err == nil { + if err := config.Ui.parse(ui); err != nil { + 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 := uiSubConfig.parse(uiSection); err != nil { + return err + } + contextualUi := UIConfigContext{ + UiConfig: uiSubConfig, + } + + var index int + switch { + case strings.Contains(sectionName, "~"): + index = strings.Index(sectionName, "~") + regex := string(sectionName[index+1:]) + contextualUi.Regex, err = regexp.Compile(regex) + if err != nil { + return err + } + case 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 + } + default: + 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) + } + + // append default paths to styleset-dirs + for _, dir := range SearchDirs { + config.Ui.StyleSetDirs = append( + config.Ui.StyleSetDirs, path.Join(dir, "stylesets"), + ) + } + + if err := config.Ui.loadStyleSet(config.Ui.StyleSetDirs); err != nil { + return err + } + + for idx, contextualUi := range config.ContextualUis { + if contextualUi.UiConfig.StyleSetName == "" && + len(contextualUi.UiConfig.StyleSetDirs) == 0 { + continue // no need to do anything if nothing is overridden + } + // fill in the missing part from the base + if contextualUi.UiConfig.StyleSetName == "" { + config.ContextualUis[idx].UiConfig.StyleSetName = config.Ui.StyleSetName + } else if len(contextualUi.UiConfig.StyleSetDirs) == 0 { + config.ContextualUis[idx].UiConfig.StyleSetDirs = config.Ui.StyleSetDirs + } + // since at least one of them has changed, load the styleset + if err := config.ContextualUis[idx].UiConfig.loadStyleSet( + config.ContextualUis[idx].UiConfig.StyleSetDirs); err != nil { + return err + } + } + + logging.Debugf("aerc.conf: [ui] %#v", config.Ui) + + return nil +} + +func (config *UIConfig) parse(section *ini.Section) error { + if err := section.MapTo(config); 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 + } + + return nil +} + +func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error { + ui.style = NewStyleSet() + err := ui.style.LoadStyleSet(ui.StyleSetName, styleSetDirs) + if err != nil { + return fmt.Errorf("Unable to load default styleset: %w", err) + } + + return nil +} + +func (config *AercConfig) mergeContextualUi(baseUi UIConfig, + contextType ContextType, s string, +) UIConfig { + for _, contextualUi := range config.ContextualUis { + if contextualUi.ContextType != contextType { + continue + } + + if !contextualUi.Regex.Match([]byte(s)) { + continue + } + + err := mergo.Merge(&baseUi, contextualUi.UiConfig, mergo.WithOverride) + if err != nil { + logging.Warnf("merge ui failed: %v", err) + } + if contextualUi.UiConfig.StyleSetName != "" { + baseUi.style = contextualUi.UiConfig.style + } + return baseUi + } + + return baseUi +} + +func (uiConfig *UIConfig) GetStyle(so StyleObject) tcell.Style { + return uiConfig.style.Get(so) +} + +func (uiConfig *UIConfig) GetStyleSelected(so StyleObject) tcell.Style { + return uiConfig.style.Selected(so) +} + +func (uiConfig *UIConfig) GetComposedStyle(base StyleObject, + styles []StyleObject, +) tcell.Style { + return uiConfig.style.Compose(base, styles) +} + +func (uiConfig *UIConfig) GetComposedStyleSelected( + base StyleObject, styles []StyleObject, +) tcell.Style { + return uiConfig.style.ComposeSelected(base, styles) +} + +func (config *AercConfig) GetUiConfig(params map[ContextType]string) *UIConfig { + baseUi := config.Ui + + for k, v := range params { + baseUi = config.mergeContextualUi(baseUi, k, v) + } + + return &baseUi +} + +func (config *AercConfig) GetContextualUIConfigs() []UIConfigContext { + return config.ContextualUis +} |