diff options
author | Robin Jarry <robin@jarry.cc> | 2023-02-03 13:14:35 +0100 |
---|---|---|
committer | Robin Jarry <robin@jarry.cc> | 2023-02-20 14:48:42 +0100 |
commit | 6cfbc87d8ab0be8d264d81b0b1f26e7b96719dfc (patch) | |
tree | c09b6b671bd90504ce0e90512b39b31f650f37fb | |
parent | d74400ac07a9f149e89fdf2b7232ffc6871f8553 (diff) | |
download | aerc-6cfbc87d8ab0be8d264d81b0b1f26e7b96719dfc.tar.gz |
dirlist: use templates instead of % mini language
Replace dirlist-format with two settings: dirlist-left & dirlist-right.
These two settings take aerc-templates(7) and may be left empty.
Add automatic translation of dirlist-format to these new settings.
Display a warning on startup if dirlist-format has been converted.
Signed-off-by: Robin Jarry <robin@jarry.cc>
Acked-by: Tim Culverhouse <tim@timculverhouse.com>
-rw-r--r-- | config/aerc.conf | 13 | ||||
-rw-r--r-- | config/templates.go | 1 | ||||
-rw-r--r-- | config/ui.go | 101 | ||||
-rw-r--r-- | doc/aerc-config.5.scd | 28 | ||||
-rw-r--r-- | doc/aerc-templates.7.scd | 2 | ||||
-rw-r--r-- | lib/state/templates.go | 14 | ||||
-rw-r--r-- | models/templates.go | 1 | ||||
-rw-r--r-- | widgets/dirlist.go | 175 | ||||
-rw-r--r-- | widgets/dirtree.go | 55 |
9 files changed, 243 insertions, 147 deletions
diff --git a/config/aerc.conf b/config/aerc.conf index 38049b77..d4e964a9 100644 --- a/config/aerc.conf +++ b/config/aerc.conf @@ -136,10 +136,17 @@ # Default: ` #pinned-tab-marker='`' -# Describes the format string to use for the directory list +# Template for the left side of the directory list. +# See aerc-templates(7) for all available fields and functions. # -# Default: %n %>r -#dirlist-format=%n %>r +# Default: {{.Folder}} +#dirlist-left={{.Folder}} + +# Template for the right side of the directory list. +# See aerc-templates(7) for all available fields and functions. +# +# Default: {{if .Unread}}{{humanReadable .Unread}}/{{end}}{{if .Exists}}{{humanReadable .Exists}}{{end}} +#dirlist-right={{if .Unread}}{{humanReadable .Unread}}/{{end}}{{if .Exists}}{{humanReadable .Exists}}{{end}} # Delay after which the messages are actually listed when entering a directory. # This avoids loading messages when skipping over folders and makes the UI more diff --git a/config/templates.go b/config/templates.go index 0f3870c5..bc05be66 100644 --- a/config/templates.go +++ b/config/templates.go @@ -104,6 +104,7 @@ func (d *dummyData) OriginalHeader(string) string { return "" } func (d *dummyData) Recent(...string) int { return 1 } func (d *dummyData) Unread(...string) int { return 3 } func (d *dummyData) Exists(...string) int { return 14 } +func (d *dummyData) RUE(...string) string { return "1/3/14" } func (d *dummyData) Connected() bool { return false } func (d *dummyData) ConnectionInfo() string { return "" } func (d *dummyData) ContentInfo() string { return "" } diff --git a/config/ui.go b/config/ui.go index d4b36f34..599651d1 100644 --- a/config/ui.go +++ b/config/ui.go @@ -22,6 +22,10 @@ type UIConfig struct { // deprecated IndexFormat string `ini:"index-format"` + DirListFormat string `ini:"dirlist-format"` // deprecated + DirListLeft *template.Template `ini:"-"` + DirListRight *template.Template `ini:"-"` + AutoMarkRead bool `ini:"auto-mark-read"` TimestampFormat string `ini:"timestamp-format"` ThisDayTimeFormat string `ini:"this-day-time-format"` @@ -52,7 +56,6 @@ type UIConfig struct { IconUnknown string `ini:"icon-unknown"` IconInvalid string `ini:"icon-invalid"` IconAttachment string `ini:"icon-attachment"` - DirListFormat string `ini:"dirlist-format"` DirListDelay time.Duration `ini:"dirlist-delay"` DirListTree bool `ini:"dirlist-tree"` DirListCollapse int `ini:"dirlist-collapse"` @@ -101,15 +104,20 @@ type uiContextKey struct { value string } +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 + IndexFormat: "", // deprecated + DirListFormat: "", // deprecated IndexColumns: []*ColumnDef{ { Name: "date", @@ -135,6 +143,8 @@ func defaultUiConfig() *UIConfig { Template: subject, }, }, + DirListLeft: left, + DirListRight: right, ColumnSeparator: " ", AutoMarkRead: true, TimestampFormat: "2006-01-02 03:04 PM", @@ -162,7 +172,6 @@ func defaultUiConfig() *UIConfig { IconUnknown: "[s?]", IconInvalid: "[s!]", IconAttachment: "a", - DirListFormat: "%n %>r", DirListDelay: 200 * time.Millisecond, NextMessageOnDelete: true, CompletionDelay: 250 * time.Millisecond, @@ -357,6 +366,58 @@ 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) + l, err := templates.ParseTemplate(left, left) + if err != nil { + return err + } + r, err := templates.ParseTemplate(right, right) + if err != nil { + return err + } + config.DirListLeft = l + config.DirListRight = r + log.Warnf("%s %s", + "The dirlist-format setting has been replaced by dirlist-left and dirlist-right.", + "dirlist-format will be removed in aerc 0.17.") + w := Warning{ + Title: "DEPRECATION WARNING: [" + section.Name() + "].dirlist-format", + Body: fmt.Sprintf(` +The dirlist-format setting is deprecated. It has been replaced by dirlist-left +and dirlist-right. + +Your configuration in this instance was automatically converted to: + +[%s] +dirlist-left = %s +dirlist-right = %s + +Your configuration file was not changed. To make this change permanent and to +dismiss this deprecation warning on launch, copy the above lines into aerc.conf +and remove dirlist-format from it. See aerc-config(5) for more details. + +dirlist-format will be removed in aerc 0.17. +`, section.Name(), left, right), + } + 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) @@ -377,7 +438,7 @@ index-format will be removed in aerc 0.17. return nil } -var indexFmtRegexp = regexp.MustCompile(`%(-?\d+)?(\.\d+)?([A-Za-z%])`) +var indexFmtRegexp = regexp.MustCompile(`%(-?\d+)?(\.\d+)?([ACDFRTZadfgilnrstuv])`) func convertIndexFormat(indexFormat string) ([]*ColumnDef, error) { matches := indexFmtRegexp.FindAllStringSubmatch(indexFormat, -1) @@ -499,6 +560,38 @@ func indexVerbToTemplate(verb rune) (f, name string) { return } +func convertDirlistFormat(format string) (string, string) { + tmpl := regexp.MustCompile(`%>?[Nnr]`).ReplaceAllStringFunc( + format, + func(s string) string { + runes := []rune(s) + switch runes[len(runes)-1] { + case 'N': + s = `{{.Folder | compactDir}}` + case 'n': + s = `{{.Folder}}` + case 'r': + s = unreadExists + default: + return s + } + if strings.HasPrefix(string(runes), "%>") { + s = "%>" + s + } + return s + }, + ) + tokens := strings.SplitN(tmpl, "%>", 1) + switch len(tokens) { + case 2: + return tokens[0], tokens[1] + case 1: + return tokens[0], "" + default: + return "", "" + } +} + func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error { ui.style = NewStyleSet() err := ui.style.LoadStyleSet(ui.StyleSetName, styleSetDirs) diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd index 4d995d6b..a04f3be9 100644 --- a/doc/aerc-config.5.scd +++ b/doc/aerc-config.5.scd @@ -211,23 +211,17 @@ These options are configured in the *[ui]* section of _aerc.conf_. Example: *sort* = _from -r date_ -*dirlist-format* = _<format>_ - Describes the format string to use for the directory list. - - Default: _%n %>r_ - -[- *Format specifier* -:[ *Description* -| _%%_ -: literal % -| _%n_ -: directory name -| _%N_ -: compacted directory name -| _%r_ -: recent/unseen/total message count -| _%>X_ -: make format specifier 'X' be right justified +*dirlist-left* = _<go template>_ + Template for the left side of the directory list. + See *aerc-templates*(7) for all available fields and functions. + + Default: _{{.Folder}}_ + +*dirlist-right* = _<go template>_ + Template for the right side of the directory list. + See *aerc-templates*(7) for all available fields and functions. + + Default: _{{if .Unread}}{{humanReadable .Unread}}/{{end}}{{if .Exists}}{{humanReadable .Exists}}{{end}}_ *dirlist-delay* = _<duration>_ Delay after which the messages are actually listed when entering diff --git a/doc/aerc-templates.7.scd b/doc/aerc-templates.7.scd index 91745e8c..83b19ac9 100644 --- a/doc/aerc-templates.7.scd +++ b/doc/aerc-templates.7.scd @@ -152,6 +152,7 @@ available always. ``` {{.Recent}} {{.Unread}} {{.Exists}} + {{.RUE}} ``` Current message counts for specific folders: @@ -160,6 +161,7 @@ available always. {{.Recent "inbox"}} {{.Unread "inbox" "aerc/pending"}} {{.Exists "archive" "spam" "foo/baz" "foo/bar"}} + {{.RUE "inbox"}} ``` *Status line* diff --git a/lib/state/templates.go b/lib/state/templates.go index f37c4865..2d5e39f5 100644 --- a/lib/state/templates.go +++ b/lib/state/templates.go @@ -1,6 +1,7 @@ package state import ( + "fmt" "strings" "time" @@ -369,6 +370,19 @@ func (d *TemplateData) Exists(folders ...string) int { return e } +func (d *TemplateData) RUE(folders ...string) string { + r, u, e := d.rue(folders...) + switch { + case r > 0: + return fmt.Sprintf("%d/%d/%d", r, u, e) + case u > 0: + return fmt.Sprintf("%d/%d", u, e) + case e > 0: + return fmt.Sprintf("%d", e) + } + return "" +} + func (d *TemplateData) Connected() bool { if d.state != nil { return d.state.Connected diff --git a/models/templates.go b/models/templates.go index c07f3dbd..4886b83c 100644 --- a/models/templates.go +++ b/models/templates.go @@ -33,6 +33,7 @@ type TemplateData interface { Recent(folders ...string) int Unread(folders ...string) int Exists(folders ...string) int + RUE(folders ...string) string Connected() bool ConnectionInfo() string ContentInfo() string diff --git a/widgets/dirlist.go b/widgets/dirlist.go index 93db0763..1e1a8a61 100644 --- a/widgets/dirlist.go +++ b/widgets/dirlist.go @@ -1,13 +1,12 @@ package widgets import ( + "bytes" "context" - "fmt" "math" "os" "regexp" "sort" - "strings" "time" "github.com/gdamore/tcell/v2" @@ -16,6 +15,8 @@ import ( "git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/lib" "git.sr.ht/~rjarry/aerc/lib/format" + "git.sr.ht/~rjarry/aerc/lib/state" + "git.sr.ht/~rjarry/aerc/lib/templates" "git.sr.ht/~rjarry/aerc/lib/ui" "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/models" @@ -184,70 +185,6 @@ func (dirlist *DirectoryList) Invalidate() { ui.Invalidate() } -func (dirlist *DirectoryList) getDirString(name string, width int, recentUnseen func() string) string { - percent := false - rightJustify := false - formatted := "" - doRightJustify := func(s string) { - formatted = runewidth.FillRight(formatted, width-len(s)) - formatted = runewidth.Truncate(formatted, width-len(s), "…") - } - for _, char := range dirlist.UiConfig(name).DirListFormat { - switch char { - case '%': - if percent { - formatted += string(char) - percent = false - } else { - percent = true - } - case '>': - if percent { - rightJustify = true - } - case 'N': - name = format.CompactPath(name, os.PathSeparator) - fallthrough - case 'n': - if percent { - if rightJustify { - doRightJustify(name) - rightJustify = false - } - formatted += name - percent = false - } - case 'r': - if percent { - rString := recentUnseen() - if rightJustify { - doRightJustify(rString) - rightJustify = false - } - formatted += rString - percent = false - } - default: - formatted += string(char) - } - } - return formatted -} - -func (dirlist *DirectoryList) getRUEString(name string) string { - r, u, e := dirlist.GetRUECount(name) - rueString := "" - switch { - case r > 0: - rueString = fmt.Sprintf("%d/%d/%d", r, u, e) - case u > 0: - rueString = fmt.Sprintf("%d/%d", u, e) - case e > 0: - rueString = fmt.Sprintf("%d", e) - } - return rueString -} - // Returns the Recent, Unread, and Exist counts for the named directory func (dirlist *DirectoryList) GetRUECount(name string) (int, int, int) { msgStore, ok := dirlist.MsgStore(name) @@ -262,8 +199,9 @@ func (dirlist *DirectoryList) GetRUECount(name string) (int, int, int) { } func (dirlist *DirectoryList) Draw(ctx *ui.Context) { + uiConfig := dirlist.UiConfig("") ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', - dirlist.UiConfig("").GetStyle(config.STYLE_DIRLIST_DEFAULT)) + uiConfig.GetStyle(config.STYLE_DIRLIST_DEFAULT)) if dirlist.spinner.IsRunning() { dirlist.spinner.Draw(ctx) @@ -271,8 +209,8 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) { } if len(dirlist.dirs) == 0 { - style := dirlist.UiConfig("").GetStyle(config.STYLE_DIRLIST_DEFAULT) - ctx.Printf(0, 0, style, dirlist.UiConfig("").EmptyDirlist) + style := uiConfig.GetStyle(config.STYLE_DIRLIST_DEFAULT) + ctx.Printf(0, 0, style, uiConfig.EmptyDirlist) return } @@ -284,9 +222,14 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) { textWidth -= 1 } if textWidth < 0 { - textWidth = 0 + return } + listCtx := ctx.Subcontext(0, 0, textWidth, ctx.Height()) + var data state.TemplateData + + data.SetAccount(dirlist.acctConf) + for i, name := range dirlist.dirs { if i < dirlist.Scroll() { continue @@ -296,27 +239,13 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) { break } - dirStyle := []config.StyleObject{} - s := dirlist.getRUEString(name) - switch strings.Count(s, "/") { - case 1: - dirStyle = append(dirStyle, config.STYLE_DIRLIST_UNREAD) - case 2: - dirStyle = append(dirStyle, config.STYLE_DIRLIST_RECENT) - } - style := dirlist.UiConfig(name).GetComposedStyle( - config.STYLE_DIRLIST_DEFAULT, dirStyle) - if name == dirlist.selecting { - style = dirlist.UiConfig(name).GetComposedStyleSelected( - config.STYLE_DIRLIST_DEFAULT, dirStyle) - } - ctx.Fill(0, row, textWidth, 1, ' ', style) - - dirString := dirlist.getDirString(name, textWidth, func() string { - return s - }) - - ctx.Printf(0, row, style, dirString) + data.SetFolder(name) + data.SetRUE([]string{name}, dirlist.GetRUECount) + left, right, style := dirlist.renderDir( + name, uiConfig, &data, + name == dirlist.selecting, listCtx.Width(), + ) + listCtx.Printf(0, row, style, "%s %s", left, right) } if dirlist.NeedScrollbar() { @@ -325,6 +254,70 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) { } } +func (dirlist *DirectoryList) renderDir( + path string, conf *config.UIConfig, data *state.TemplateData, + selected bool, width int, +) (string, string, tcell.Style) { + var left, right string + var buf bytes.Buffer + + var styles []config.StyleObject + var style tcell.Style + + r, u, _ := dirlist.GetRUECount(path) + switch { + case r > 0: + styles = append(styles, config.STYLE_DIRLIST_RECENT) + case u > 0: + styles = append(styles, config.STYLE_DIRLIST_UNREAD) + } + conf = conf.ForFolder(path) + if selected { + style = conf.GetComposedStyleSelected( + config.STYLE_DIRLIST_DEFAULT, styles) + } else { + style = conf.GetComposedStyle( + config.STYLE_DIRLIST_DEFAULT, styles) + } + + err := templates.Render(conf.DirListLeft, &buf, data) + if err != nil { + log.Errorf("dirlist-left: %s", err) + left = err.Error() + style = conf.GetStyle(config.STYLE_ERROR) + } else { + left = buf.String() + } + buf.Reset() + err = templates.Render(conf.DirListRight, &buf, data) + if err != nil { + log.Errorf("dirlist-right: %s", err) + right = err.Error() + style = conf.GetStyle(config.STYLE_ERROR) + } else { + right = buf.String() + } + buf.Reset() + + lwidth := runewidth.StringWidth(left) + rwidth := runewidth.StringWidth(right) + + if lwidth+rwidth+1 > width { + if rwidth > 3*width/4 { + rwidth = 3 * width / 4 + } + lwidth = width - rwidth - 1 + right = runewidth.FillLeft(right, rwidth) + right = format.TruncateHead(right, rwidth, "…") + left = runewidth.FillRight(left, lwidth) + left = runewidth.Truncate(left, lwidth, "…") + } else { + left = runewidth.FillRight(left, width-rwidth-1) + } + + return left, right, style +} + func (dirlist *DirectoryList) drawScrollbar(ctx *ui.Context) { gutterStyle := tcell.StyleDefault pillStyle := tcell.StyleDefault.Reverse(true) diff --git a/widgets/dirtree.go b/widgets/dirtree.go index e9fbf061..0c7f090a 100644 --- a/widgets/dirtree.go +++ b/widgets/dirtree.go @@ -8,6 +8,7 @@ import ( "git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/lib" + "git.sr.ht/~rjarry/aerc/lib/state" "git.sr.ht/~rjarry/aerc/lib/ui" "git.sr.ht/~rjarry/aerc/log" "git.sr.ht/~rjarry/aerc/worker/types" @@ -51,8 +52,9 @@ func (dt *DirectoryTree) UpdateList(done func([]string)) { } func (dt *DirectoryTree) Draw(ctx *ui.Context) { + uiConfig := dt.UiConfig("") ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', - dt.UiConfig("").GetStyle(config.STYLE_DIRLIST_DEFAULT)) + uiConfig.GetStyle(config.STYLE_DIRLIST_DEFAULT)) if dt.DirectoryList.spinner.IsRunning() { dt.DirectoryList.spinner.Draw(ctx) @@ -61,8 +63,8 @@ func (dt *DirectoryTree) Draw(ctx *ui.Context) { n := dt.countVisible(dt.list) if n == 0 || dt.listIdx < 0 { - style := dt.UiConfig("").GetStyle(config.STYLE_DIRLIST_DEFAULT) - ctx.Printf(0, 0, style, dt.UiConfig("").EmptyDirlist) + style := uiConfig.GetStyle(config.STYLE_DIRLIST_DEFAULT) + ctx.Printf(0, 0, style, uiConfig.EmptyDirlist) return } @@ -80,46 +82,35 @@ func (dt *DirectoryTree) Draw(ctx *ui.Context) { textWidth -= 1 } if textWidth < 0 { - textWidth = 0 + return } + treeCtx := ctx.Subcontext(0, 0, textWidth, ctx.Height()) + var data state.TemplateData + + data.SetAccount(dt.acctConf) + + n = 0 for i, node := range dt.list { + if n > treeCtx.Height() { + break + } rowNr := dt.countVisible(dt.list[:i]) if rowNr < dt.Scroll() || !isVisible(node) { continue } - row := rowNr - dt.Scroll() - if row >= ctx.Height() { - break - } - name := dt.displayText(node) - - dirStyle := []config.StyleObject{} path := dt.getDirectory(node) - s := dt.getRUEString(path) - switch strings.Count(s, "/") { - case 1: - dirStyle = append(dirStyle, config.STYLE_DIRLIST_UNREAD) - case 2: - dirStyle = append(dirStyle, config.STYLE_DIRLIST_RECENT) - } - style := dt.UiConfig(path).GetComposedStyle( - config.STYLE_DIRLIST_DEFAULT, dirStyle) - if i == dt.listIdx { - style = dt.UiConfig(path).GetComposedStyleSelected( - config.STYLE_DIRLIST_DEFAULT, dirStyle) - } - ctx.Fill(0, row, textWidth, 1, ' ', style) + data.SetFolder(dt.displayText(node)) + data.SetRUE([]string{path}, dt.GetRUECount) - dirString := dt.getDirString(name, textWidth, func() string { - if path != "" { - return s - } - return "" - }) + left, right, style := dt.renderDir( + path, uiConfig, &data, + i == dt.listIdx, treeCtx.Width(), + ) - ctx.Printf(0, row, style, dirString) + treeCtx.Printf(0, n, style, "%s %s", left, right) + n++ } if dt.NeedScrollbar() { |