aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rw-r--r--config/aerc.conf37
-rw-r--r--config/columns.go40
-rw-r--r--config/ui.go178
-rw-r--r--config/ui_test.go46
-rw-r--r--doc/aerc-config.5.scd80
-rw-r--r--lib/templates/data.go9
-rw-r--r--widgets/aerc.go24
-rw-r--r--widgets/msglist.go207
9 files changed, 474 insertions, 149 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 269846f0..913084c2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
external command configured via `[compose].file-picker-cmd` in `aerc.conf`.
- Sample stylesets are now installed in `$PREFIX/share/aerc/stylesets`.
- The built-in `colorize` filter now has different themes.
+- New column-based message list format with `index-columns`.
### Changed
@@ -52,6 +53,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Deprecated
- Removed broken `:set` command.
+- `[ui].index-format` setting has been replaced by `index-columns`.
## [0.13.0](https://git.sr.ht/~rjarry/aerc/refs/0.13.0) - 2022-10-20
diff --git a/config/aerc.conf b/config/aerc.conf
index 05ebbf41..1ce4810e 100644
--- a/config/aerc.conf
+++ b/config/aerc.conf
@@ -40,11 +40,38 @@
[ui]
#
-# Describes the format for each row in a mailbox view. This field is compatible
-# with mutt's printf-like syntax.
-#
-# Default: %-20.20D %-17.17n %Z %s
-#index-format=%-20.20D %-17.17n %Z %s
+# Describes the format for each row in a mailbox view. This is a comma
+# separated list of column names with an optional align and width suffix. After
+# the column name, one of the '<' (left), ':' (center) or '>' (right) alignment
+# characters can be added (by default, left) followed by an optional width
+# specifier. The width is either an integer representing a fixed number of
+# characters, or a percentage between 1% and 99% representing a fraction of the
+# terminal width. It can also be one of the '*' (auto) or '=' (fit) special
+# width specifiers. Auto width columns will be equally attributed the remaining
+# terminal width. Fit width columns take the width of their contents. If no
+# width specifier is set, '*' is used by default.
+#
+# Default: date<20,name<17,flags>4,subject<*
+#index-columns=date<20,name<17,flags>4,subject<*
+
+#
+# Each name in index-columns must have a corresponding column-$name setting.
+# All column-$name settings accept golang text/template syntax. See
+# aerc-templates(7) for available template attributes and functions.
+#
+# Default settings
+#column-date={{.DateAutoFormat .Date.Local}}
+#column-name={{index (.From | names) 0}}
+#column-flags={{.Flags | join ""}}
+#column-subject={{.Subject}}
+
+#
+# String separator inserted between columns. When the column width specifier is
+# an exact number of characters, the separator is added to it (i.e. the exact
+# width will be fully available for the column contents).
+#
+# Default: " "
+#column-separator=" "
#
# See time.Time#Format at https://godoc.org/time#Time.Format
diff --git a/config/columns.go b/config/columns.go
index 659be2c3..b1c7191b 100644
--- a/config/columns.go
+++ b/config/columns.go
@@ -3,6 +3,7 @@ package config
import (
"bytes"
"fmt"
+ "reflect"
"regexp"
"strconv"
"strings"
@@ -118,3 +119,42 @@ func ParseColumnDefs(key *ini.Key, section *ini.Section) ([]*ColumnDef, error) {
}
return columns, nil
}
+
+func ColumnDefsToIni(defs []*ColumnDef, keyName string) string {
+ var s strings.Builder
+ var cols []string
+ templates := make(map[string]string)
+
+ for _, def := range defs {
+ col := def.Name
+ switch {
+ case def.Flags.Has(ALIGN_LEFT):
+ col += "<"
+ case def.Flags.Has(ALIGN_CENTER):
+ col += ":"
+ case def.Flags.Has(ALIGN_RIGHT):
+ col += ">"
+ }
+ switch {
+ case def.Flags.Has(WIDTH_FIT):
+ col += "="
+ case def.Flags.Has(WIDTH_AUTO):
+ col += "*"
+ case def.Flags.Has(WIDTH_FRACTION):
+ col += fmt.Sprintf("%.0f%%", def.Width*100)
+ default:
+ col += fmt.Sprintf("%.0f", def.Width)
+ }
+ cols = append(cols, col)
+ tree := reflect.ValueOf(def.Template.Tree)
+ text := tree.Elem().FieldByName("text").String()
+ templates[fmt.Sprintf("column-%s", def.Name)] = text
+ }
+
+ s.WriteString(fmt.Sprintf("%s = %s\n", keyName, strings.Join(cols, ",")))
+ for name, text := range templates {
+ s.WriteString(fmt.Sprintf("%s = %s\n", name, text))
+ }
+
+ return s.String()
+}
diff --git a/config/ui.go b/config/ui.go
index 18724af2..98b9f629 100644
--- a/config/ui.go
+++ b/config/ui.go
@@ -4,9 +4,11 @@ import (
"fmt"
"path"
"regexp"
+ "strconv"
"strings"
"time"
+ "git.sr.ht/~rjarry/aerc/lib/templates"
"git.sr.ht/~rjarry/aerc/log"
"github.com/gdamore/tcell/v2"
"github.com/go-ini/ini"
@@ -14,8 +16,12 @@ import (
)
type UIConfig struct {
+ IndexColumns []*ColumnDef `ini:"-"`
+ ColumnSeparator string `ini:"column-separator"`
+ // deprecated
+ IndexFormat string `ini:"index-format"`
+
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"`
@@ -92,9 +98,39 @@ type uiContextKey struct {
}
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}}")
return &UIConfig{
+ IndexFormat: "", // 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,
+ },
+ },
+ ColumnSeparator: " ",
AutoMarkRead: true,
- IndexFormat: "%-20.20D %-17.17n %Z %s",
TimestampFormat: "2006-01-02 03:04 PM",
ThisDayTimeFormat: "",
ThisWeekTimeFormat: "",
@@ -283,9 +319,147 @@ func (config *UIConfig) parse(section *ini.Section) error {
config.MessageViewThisDayTimeFormat = config.TimestampFormat
}
+ if config.IndexFormat != "" {
+ log.Warnf("%s %s",
+ "The index-format setting has been replaced by index-columns.",
+ "index-format will be removed in aerc 0.17.")
+ }
+ if key, err := section.GetKey("index-columns"); err == nil {
+ columns, err := ParseColumnDefs(key, section)
+ if err != nil {
+ return err
+ }
+ config.IndexColumns = columns
+ config.IndexFormat = "" // to silence popup at startup
+ } else if config.IndexFormat != "" {
+ columns, err := convertIndexFormat(config.IndexFormat)
+ if err != nil {
+ return err
+ }
+ config.IndexColumns = columns
+ }
+
return nil
}
+var indexFmtRegexp = regexp.MustCompile(`%(-?\d+)?(\.\d+)?([A-Za-z%])`)
+
+func convertIndexFormat(indexFormat string) ([]*ColumnDef, error) {
+ matches := indexFmtRegexp.FindAllStringSubmatch(indexFormat, -1)
+ if matches == nil {
+ return nil, fmt.Errorf("invalid index-format")
+ }
+
+ var columns []*ColumnDef
+
+ for _, m := range matches {
+ alignWidth := m[1]
+ verb := m[3]
+
+ var f string
+ var width float64 = 0
+ var flags ColumnFlags = ALIGN_LEFT
+ name := ""
+
+ switch verb {
+ case "%":
+ f = verb
+ case "a":
+ f = `{{(index .From 0).Address}}`
+ name = "sender"
+ case "A":
+ f = `{{if eq (len .ReplyTo) 0}}{{(index .From 0).Address}}{{else}}{{(index .ReplyTo 0).Address}}{{end}}`
+ name = "reply-to"
+ case "C":
+ f = "{{.Number}}"
+ name = "num"
+ case "d", "D":
+ f = "{{.DateAutoFormat .Date.Local}}"
+ name = "date"
+ case "f":
+ f = `{{index (.From | persons) 0}}`
+ name = "from"
+ case "F":
+ f = `{{.Peer | names | join ", "}}`
+ name = "peers"
+ case "g":
+ f = `{{.Labels | join ", "}}`
+ name = "labels"
+ case "i":
+ f = "{{.MessageId}}"
+ name = "msg-id"
+ case "n":
+ f = `{{index (.From | names) 0}}`
+ name = "name"
+ case "r":
+ f = `{{.To | persons | join ", "}}`
+ name = "to"
+ case "R":
+ f = `{{.Cc | persons | join ", "}}`
+ name = "cc"
+ case "s":
+ f = "{{.Subject}}"
+ name = "subject"
+ case "t":
+ f = "{{(index .To 0).Address}}"
+ name = "to0"
+ case "T":
+ f = "{{.Account}}"
+ name = "account"
+ case "u":
+ f = "{{index (.From | mboxes) 0}}"
+ name = "mboxes"
+ case "v":
+ f = "{{index (.From | names) 0}}"
+ name = "name"
+ case "Z":
+ f = `{{.Flags | join ""}}`
+ name = "flags"
+ width = 4
+ flags = ALIGN_RIGHT
+ case "l":
+ f = "{{.Size}}"
+ name = "size"
+ default:
+ f = "%" + verb
+ }
+ if name == "" {
+ name = "wtf"
+ }
+
+ t, err := templates.ParseTemplate(fmt.Sprintf("column-%s", name), f)
+ if err != nil {
+ return nil, err
+ }
+
+ if alignWidth != "" {
+ width, err = strconv.ParseFloat(alignWidth, 64)
+ if err != nil {
+ return nil, err
+ }
+ if width < 0 {
+ width = -width
+ } else {
+ flags = ALIGN_RIGHT
+ }
+ }
+ if width == 0 {
+ flags |= WIDTH_AUTO
+ } else {
+ flags |= WIDTH_EXACT
+ }
+
+ columns = append(columns, &ColumnDef{
+ Name: name,
+ Width: width,
+ Flags: flags,
+ Template: t,
+ })
+ }
+
+ return columns, nil
+}
+
func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error {
ui.style = NewStyleSet()
err := ui.style.LoadStyleSet(ui.StyleSetName, styleSetDirs)
diff --git a/config/ui_test.go b/config/ui_test.go
new file mode 100644
index 00000000..c5eb6031
--- /dev/null
+++ b/config/ui_test.go
@@ -0,0 +1,46 @@
+package config
+
+import (
+ "bytes"
+ "testing"
+
+ "git.sr.ht/~rjarry/aerc/lib/templates"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestConvertIndexFormat(t *testing.T) {
+ columns, err := convertIndexFormat("%-20.20D %-17.17n %Z %s")
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Len(t, columns, 4)
+
+ data := templates.DummyData()
+ var buf bytes.Buffer
+
+ assert.Equal(t, "date", columns[0].Name)
+ assert.Equal(t, 20.0, columns[0].Width)
+ assert.Equal(t, ALIGN_LEFT|WIDTH_EXACT, columns[0].Flags)
+ assert.Nil(t, columns[0].Template.Execute(&buf, data))
+
+ buf.Reset()
+ assert.Equal(t, "name", columns[1].Name)
+ assert.Equal(t, 17.0, columns[1].Width)
+ assert.Equal(t, ALIGN_LEFT|WIDTH_EXACT, columns[1].Flags)
+ assert.Nil(t, columns[1].Template.Execute(&buf, data))
+ assert.Equal(t, "John Doe", buf.String())
+
+ buf.Reset()
+ assert.Equal(t, "flags", columns[2].Name)
+ assert.Equal(t, 4.0, columns[2].Width)
+ assert.Equal(t, ALIGN_RIGHT|WIDTH_EXACT, columns[2].Flags)
+ assert.Nil(t, columns[2].Template.Execute(&buf, data))
+ assert.Equal(t, "O!*", buf.String())
+
+ buf.Reset()
+ assert.Equal(t, "subject", columns[3].Name)
+ assert.Equal(t, 0.0, columns[3].Width)
+ assert.Equal(t, ALIGN_LEFT|WIDTH_AUTO, columns[3].Flags)
+ assert.Nil(t, columns[3].Template.Execute(&buf, data))
+ assert.Equal(t, "[PATCH aerc 2/3] foo: baz bar buz", buf.String())
+}
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index dcaa3dd4..7e189a48 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -66,53 +66,43 @@ These options are configured in the *[general]* section of _aerc.conf_.
These options are configured in the *[ui]* section of _aerc.conf_.
-*index-format* = _<format>_
- Describes the format for each row in a mailbox view. This field is
- compatible with mutt's printf-like syntax.
+*index-columns* = _<column1,column2,column3...>_
+ Describes the format for each row in a mailbox view. This is a comma
+ separated list of column names with an optional align and width suffix.
+ After the column name, one of the _<_ (left), _:_ (center) or _>_
+ (right) alignment characters can be added (by default, left) followed by
+ an optional width specifier. The width is either an integer representing
+ a fixed number of characters, or a percentage between _1%_ and _99%_
+ representing a fraction of the terminal width. It can also be one of the
+ _\*_ (auto) or _=_ (fit) special width specifiers. Auto width columns
+ will be equally attributed the remaining terminal width. Fit width
+ columns take the width of their contents. If no width specifier is set,
+ _\*_ is used by default.
- Default: _%D %-17.17n %s_
+ Default: _date<20,name<17,flags>4,subject<\*_
-[- *Format specifier*
-:[ *Description*
-| _%%_
-: literal %
-| _%a_
-: sender address
-| _%A_
-: reply-to address, or sender address if none
-| _%C_
-: message number
-| _%d_
-: formatted message timestamp
-| _%D_
-: formatted message timestamp converted to local timezone
-| _%f_
-: sender name and address
-| _%F_
-: author name, or recipient name if the message is from you.
- The address is shown if no name part.
-| _%g_
-: message labels (for example notmuch tags)
-| _%i_
-: message id
-| _%n_
-: sender name, or sender address if none
-| _%r_
-: comma-separated list of formatted recipient names and addresses
-| _%R_
-: comma-separated list of formatted CC names and addresses
-| _%s_
-: subject
-| _%t_
-: the (first) address the new email was sent to
-| _%T_
-: the account name which received the email
-| _%u_
-: sender mailbox name (e.g. "smith" in "smith@example.net")
-| _%v_
-: sender first name (e.g. "Alex" in "Alex Smith <smith@example.net>")
-| _%Z_
-: flags (O=old, N=new, r=answered, D=deleted, !=flagged, \*=marked, a=attachment)
+*column-separator* = _"<separator>"_
+ String separator inserted between columns. When a column width specifier
+ is an exact number of characters, the separator is added to it (i.e. the
+ exact width will be fully available for that column contents).
+
+ Default: _" "_
+
+*column-<name>* = _<go template>_
+ Each name in *index-columns* must have a corresponding *column-<name>*
+ setting. All *column-<name>* settings accept golang text/template
+ syntax.
+
+ By default, these columns are defined:
+
+ ```
+ column-date = {{.DateAutoFormat .Date.Local}}
+ column-name = {{index (.From | names) 0}}
+ column-flags = {{.Flags | join ""}}
+ column-subject = {{.Subject}}
+ ```
+
+ See *aerc-templates*(7) for all available symbols and functions.
*timestamp-format* = _<timeformat>_
See time.Time#Format at https://godoc.org/time#Time.Format
diff --git a/lib/templates/data.go b/lib/templates/data.go
index dab698d5..cb69d8fe 100644
--- a/lib/templates/data.go
+++ b/lib/templates/data.go
@@ -20,6 +20,10 @@ type TemplateData struct {
marked bool
msgNum int
+ // message list threading
+ ThreadSameSubject bool
+ ThreadPrefix string
+
// account config
myAddresses map[string]bool
account string
@@ -215,7 +219,10 @@ func (d *TemplateData) Subject() string {
case d.headers != nil:
subject = d.Header("subject")
}
- return subject
+ if d.ThreadSameSubject {
+ subject = ""
+ }
+ return d.ThreadPrefix + subject
}
func (d *TemplateData) SubjectBase() string {
diff --git a/widgets/aerc.go b/widgets/aerc.go
index eb904ea0..59944cc6 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -126,6 +126,30 @@ func NewAerc(
}
}
+ if config.Ui.IndexFormat != "" {
+ ini := config.ColumnDefsToIni(
+ config.Ui.IndexColumns, "index-columns")
+ title := "DEPRECATION WARNING"
+ text := `
+The index-format setting is deprecated. It has been replaced by index-columns.
+
+Your configuration in this instance was automatically converted to:
+
+[ui]
+` + ini + `
+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 index-format from it. See aerc-config(5) for more details.
+
+index-format will be removed in aerc 0.17.
+`
+ aerc.AddDialog(NewSelectorDialog(
+ title, text, []string{"OK"}, 0,
+ aerc.SelectedAccountUiConfig(),
+ func(string, error) { aerc.CloseDialog() },
+ ))
+ }
+
return aerc
}
diff --git a/widgets/msglist.go b/widgets/msglist.go
index 839453f2..169fb310 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -1,18 +1,17 @@
package widgets
import (
+ "bytes"
"fmt"
"math"
"strings"
- sortthread "github.com/emersion/go-imap-sortthread"
"github.com/gdamore/tcell/v2"
- "github.com/mattn/go-runewidth"
"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/iterator"
+ "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"
@@ -45,6 +44,13 @@ func (ml *MessageList) Invalidate() {
ui.Invalidate()
}
+type messageRowParams struct {
+ uid uint32
+ needsHeaders bool
+ uiConfig *config.UIConfig
+ styles []config.StyleObject
+}
+
func (ml *MessageList) Draw(ctx *ui.Context) {
ml.height = ctx.Height()
ml.width = ctx.Width()
@@ -54,25 +60,22 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
acct := ml.aerc.SelectedAccount()
store := ml.Store()
- if store == nil || acct == nil {
+ if store == nil || acct == nil || len(store.Uids()) == 0 {
if ml.isInitalizing {
ml.spinner.Draw(ctx)
- return
} else {
ml.spinner.Stop()
ml.drawEmptyMessage(ctx)
- return
}
+ return
}
ml.UpdateScroller(ml.height, len(store.Uids()))
- if store := ml.Store(); store != nil && len(store.Uids()) > 0 {
- iter := store.UidsIterator()
- for i := 0; iter.Next(); i++ {
- if store.SelectedUid() == iter.Value().(uint32) {
- ml.EnsureScroll(i)
- break
- }
+ iter := store.UidsIterator()
+ for i := 0; iter.Next(); i++ {
+ if store.SelectedUid() == iter.Value().(uint32) {
+ ml.EnsureScroll(i)
+ break
}
}
@@ -80,25 +83,57 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
if ml.NeedScrollbar() {
textWidth -= 1
}
- if textWidth < 0 {
- textWidth = 0
+ if textWidth <= 0 {
+ return
}
- var (
- needsHeaders []uint32
- row int = 0
+ data := templates.NewTemplateData(
+ acct.acct.From,
+ acct.acct.Aliases,
+ acct.Name(),
+ acct.Directories().Selected(),
+ uiConfig.TimestampFormat,
+ uiConfig.ThisDayTimeFormat,
+ uiConfig.ThisWeekTimeFormat,
+ uiConfig.ThisYearTimeFormat,
+ uiConfig.IconAttachment,
)
- createBaseCtx := func(uid uint32, row int) format.Ctx {
- return format.Ctx{
- FromAddress: format.AddressForHumans(acct.acct.From),
- AccountName: acct.Name(),
- MsgInfo: store.Messages[uid],
- MsgNum: row,
- MsgIsMarked: store.Marker().IsMarked(uid),
+ var needsHeaders []uint32
+
+ customDraw := func(t *ui.Table, r int, c *ui.Context) bool {
+ row := &t.Rows[r]
+ params, _ := row.Priv.(messageRowParams)
+ if params.needsHeaders {
+ needsHeaders = append(needsHeaders, params.uid)
+ ml.spinner.Draw(ctx.Subcontext(0, r, t.Width, 1))
+ return true
+ }
+ return false
+ }
+
+ getRowStyle := func(t *ui.Table, r int) tcell.Style {
+ var style tcell.Style
+ row := &t.Rows[r]
+ params, _ := row.Priv.(messageRowParams)
+ if params.uid == store.SelectedUid() {
+ style = params.uiConfig.GetComposedStyleSelected(
+ config.STYLE_MSGLIST_DEFAULT, params.styles)
+ } else {
+ style = params.uiConfig.GetComposedStyle(
+ config.STYLE_MSGLIST_DEFAULT, params.styles)
}
+ return style
}
+ table := ui.NewTable(
+ textWidth, ml.height,
+ uiConfig.IndexColumns,
+ uiConfig.ColumnSeparator,
+ customDraw,
+ getRowStyle,
+ )
+
if store.ThreadedView() {
var (
lastSubject string
@@ -126,23 +161,22 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
i++
continue
}
- if thread := curIter.Value().(*types.Thread); thread != nil {
- fmtCtx := createBaseCtx(thread.Uid, row)
- fmtCtx.ThreadPrefix = threadPrefix(thread,
- store.ReverseThreadOrder())
- if fmtCtx.MsgInfo != nil && fmtCtx.MsgInfo.Envelope != nil {
- baseSubject, _ := sortthread.GetBaseSubject(
- fmtCtx.MsgInfo.Envelope.Subject)
- fmtCtx.ThreadSameSubject = baseSubject == lastSubject &&
- sameParent(thread, prevThread) &&
- !isParent(thread)
- lastSubject = baseSubject
- prevThread = thread
- }
- if ml.drawRow(textWidth, ctx, thread.Uid, row, &needsHeaders, fmtCtx) {
- break threadLoop
- }
- row += 1
+ thread := curIter.Value().(*types.Thread)
+ if thread == nil {
+ continue
+ }
+
+ baseSubject := data.SubjectBase()
+ data.ThreadSameSubject = baseSubject == lastSubject &&
+ sameParent(thread, prevThread) &&
+ !isParent(thread)
+ data.ThreadPrefix = threadPrefix(thread,
+ store.ReverseThreadOrder())
+ lastSubject = baseSubject
+ prevThread = thread
+
+ if addMessage(store, thread.Uid, &table, data, uiConfig) {
+ break threadLoop
}
}
}
@@ -153,14 +187,14 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
continue
}
uid := iter.Value().(uint32)
- fmtCtx := createBaseCtx(uid, row)
- if ml.drawRow(textWidth, ctx, uid, row, &needsHeaders, fmtCtx) {
+ if addMessage(store, uid, &table, data, uiConfig) {
break
}
- row += 1
}
}
+ table.Draw(ctx)
+
if ml.NeedScrollbar() {
scrollbarCtx := ctx.Subcontext(ctx.Width()-1, 0, 1, ctx.Height())
ml.drawScrollbar(scrollbarCtx)
@@ -184,79 +218,60 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
}
}
-func (ml *MessageList) drawRow(textWidth int, ctx *ui.Context, uid uint32, row int, needsHeaders *[]uint32, fmtCtx format.Ctx) bool {
- store := ml.store
+func addMessage(
+ store *lib.MessageStore, uid uint32,
+ table *ui.Table, data *templates.TemplateData,
+ uiConfig *config.UIConfig,
+) bool {
msg := store.Messages[uid]
- acct := ml.aerc.SelectedAccount()
- if row >= ctx.Height() || acct == nil {
- return true
- }
-
- if msg == nil {
- *needsHeaders = append(*needsHeaders, uid)
- ml.spinner.Draw(ctx.Subcontext(0, row, textWidth, 1))
- return false
- }
+ cells := make([]string, len(table.Columns))
+ params := messageRowParams{uid: uid}
- // TODO deprecate subject contextual UIs? Only related setting is styleset,
- // should implement a better per-message styling method
- // Check if we have any applicable ContextualUIConfigs
- uiConfig := acct.Directories().UiConfig(store.DirInfo.Name)
- if msg.Envelope != nil {
- uiConfig = uiConfig.ForSubject(msg.Envelope.Subject)
+ if msg == nil || msg.Envelope == nil {
+ params.needsHeaders = true
+ return table.AddRow(cells, params)
}
- msg_styles := []config.StyleObject{}
if msg.Flags.Has(models.SeenFlag) {
- msg_styles = append(msg_styles, config.STYLE_MSGLIST_READ)
+ params.styles = append(params.styles, config.STYLE_MSGLIST_READ)
} else {
- msg_styles = append(msg_styles, config.STYLE_MSGLIST_UNREAD)
+ params.styles = append(params.styles, config.STYLE_MSGLIST_UNREAD)
}
-
if msg.Flags.Has(models.FlaggedFlag) {
- msg_styles = append(msg_styles, config.STYLE_MSGLIST_FLAGGED)
+ params.styles = append(params.styles, config.STYLE_MSGLIST_FLAGGED)
}
-
// deleted message
if _, ok := store.Deleted[msg.Uid]; ok {
- msg_styles = append(msg_styles, config.STYLE_MSGLIST_DELETED)
+ params.styles = append(params.styles, config.STYLE_MSGLIST_DELETED)
}
// search result
if store.IsResult(msg.Uid) {
- msg_styles = append(msg_styles, config.STYLE_MSGLIST_RESULT)
+ params.styles = append(params.styles, config.STYLE_MSGLIST_RESULT)
}
-
// marked message
- if store.Marker().IsMarked(msg.Uid) {
- msg_styles = append(msg_styles, config.STYLE_MSGLIST_MARKED)
+ marked := store.Marker().IsMarked(msg.Uid)
+ if marked {
+ params.styles = append(params.styles, config.STYLE_MSGLIST_MARKED)
}
- var style tcell.Style
- // current row
- if msg.Uid == ml.store.SelectedUid() {
- style = uiConfig.GetComposedStyleSelected(config.STYLE_MSGLIST_DEFAULT, msg_styles)
- } else {
- style = uiConfig.GetComposedStyle(config.STYLE_MSGLIST_DEFAULT, msg_styles)
- }
+ data.SetInfo(msg, len(table.Rows), marked)
- ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
- fmtStr, args, err := format.ParseMessageFormat(
- uiConfig.IndexFormat, uiConfig.TimestampFormat,
- uiConfig.ThisDayTimeFormat,
- uiConfig.ThisWeekTimeFormat,
- uiConfig.ThisYearTimeFormat,
- uiConfig.IconAttachment,
- fmtCtx)
- if err != nil {
- ctx.Printf(0, row, style, "%v", err)
- } else {
- line := fmt.Sprintf(fmtStr, args...)
- line = runewidth.Truncate(line, textWidth, "…")
- ctx.Printf(0, row, style, "%s", line)
+ for c, col := range table.Columns {
+ var buf bytes.Buffer
+ err := col.Def.Template.Execute(&buf, data)
+ if err != nil {
+ cells[c] = err.Error()
+ } else {
+ cells[c] = buf.String()
+ }
}
- return false
+ // TODO deprecate subject contextual UIs? Only related setting is
+ // styleset, should implement a better per-message styling method
+ params.uiConfig = uiConfig.ForSubject(msg.Envelope.Subject)
+
+ return table.AddRow(cells, params)
}
func (ml *MessageList) drawScrollbar(ctx *ui.Context) {