From 535300cfdbfc6e72fe9717c409fa64f072a1c581 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Tue, 6 Sep 2022 07:33:21 +0200 Subject: config: add columns based index format The index-format option comes from mutt and is neither user friendly, nor intuitive. Introduce a new way of configuring the message list contents. Replace index-format with multiple settings to make everything more intuitive. Reuse the table widget added in the previous commit. index-columns Comma-separated list of column names followed by optional alignment and width specifiers. column-separator String separator between columns. column-$name One setting for every name defined in index-columns. This supports golang text/template syntax and allows access to the same message information than before and much more. When index-format is still defined in aerc.conf (which will most likely happen when users will update after this patch), convert it to the new index-columns + column-$name and column-separator system and a warning is displayed on startup so that users are aware that they need to update their config. Signed-off-by: Robin Jarry Acked-by: Tim Culverhouse Tested-by: Bence Ferdinandy --- CHANGELOG.md | 2 + config/aerc.conf | 37 +++++++-- config/columns.go | 40 ++++++++++ config/ui.go | 178 ++++++++++++++++++++++++++++++++++++++++++- config/ui_test.go | 46 +++++++++++ doc/aerc-config.5.scd | 80 +++++++++---------- lib/templates/data.go | 9 ++- widgets/aerc.go | 24 ++++++ widgets/msglist.go | 207 +++++++++++++++++++++++++++----------------------- 9 files changed, 474 insertions(+), 149 deletions(-) create mode 100644 config/ui_test.go 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* = __ - Describes the format for each row in a mailbox view. This field is - compatible with mutt's printf-like syntax. +*index-columns* = __ + 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 ") -| _%Z_ -: flags (O=old, N=new, r=answered, D=deleted, !=flagged, \*=marked, a=attachment) +*column-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-* = __ + Each name in *index-columns* must have a corresponding *column-* + setting. All *column-* 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* = __ 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) { -- cgit