mirror of
https://git.sr.ht/~rjarry/aerc
synced 2026-03-02 18:23:33 +01:00
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 <robin@jarry.cc> Acked-by: Tim Culverhouse <tim@timculverhouse.com> Tested-by: Bence Ferdinandy <bence@ferdinandy.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
# 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: %-20.20D %-17.17n %Z %s
|
||||
#index-format=%-20.20D %-17.17n %Z %s
|
||||
# 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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
178
config/ui.go
178
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)
|
||||
|
||||
46
config/ui_test.go
Normal file
46
config/ui_test.go
Normal file
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
cells := make([]string, len(table.Columns))
|
||||
params := messageRowParams{uid: uid}
|
||||
|
||||
if msg == nil || msg.Envelope == nil {
|
||||
params.needsHeaders = true
|
||||
return table.AddRow(cells, params)
|
||||
}
|
||||
|
||||
if msg == nil {
|
||||
*needsHeaders = append(*needsHeaders, uid)
|
||||
ml.spinner.Draw(ctx.Subcontext(0, row, textWidth, 1))
|
||||
return false
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
// 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 false
|
||||
return table.AddRow(cells, params)
|
||||
}
|
||||
|
||||
func (ml *MessageList) drawScrollbar(ctx *ui.Context) {
|
||||
|
||||
Reference in New Issue
Block a user