Message list: implement index-format option
This commit is contained in:
parent
6d491569c0
commit
fca7321639
8 changed files with 294 additions and 55 deletions
|
@ -6,7 +6,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
gomail "net/mail"
|
gomail "net/mail"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.sr.ht/~sircmpwn/getopt"
|
"git.sr.ht/~sircmpwn/getopt"
|
||||||
|
@ -15,6 +14,7 @@ import (
|
||||||
_ "github.com/emersion/go-message/charset"
|
_ "github.com/emersion/go-message/charset"
|
||||||
"github.com/emersion/go-message/mail"
|
"github.com/emersion/go-message/mail"
|
||||||
|
|
||||||
|
"git.sr.ht/~sircmpwn/aerc/lib"
|
||||||
"git.sr.ht/~sircmpwn/aerc/widgets"
|
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -23,25 +23,6 @@ func init() {
|
||||||
register("forward", Reply)
|
register("forward", Reply)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
atom *regexp.Regexp = regexp.MustCompile("^[a-z0-9!#$%7'*+-/=?^_`{}|~ ]+$")
|
|
||||||
)
|
|
||||||
|
|
||||||
func formatAddress(addr *imap.Address) string {
|
|
||||||
if addr.PersonalName != "" {
|
|
||||||
if atom.MatchString(addr.PersonalName) {
|
|
||||||
return fmt.Sprintf("%s <%s@%s>",
|
|
||||||
addr.PersonalName, addr.MailboxName, addr.HostName)
|
|
||||||
} else {
|
|
||||||
return fmt.Sprintf("\"%s\" <%s@%s>",
|
|
||||||
strings.ReplaceAll(addr.PersonalName, "\"", "'"),
|
|
||||||
addr.MailboxName, addr.HostName)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Sprintf("<%s@%s>", addr.MailboxName, addr.HostName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Reply(aerc *widgets.Aerc, args []string) error {
|
func Reply(aerc *widgets.Aerc, args []string) error {
|
||||||
opts, optind, err := getopt.Getopts(args[1:], "aq")
|
opts, optind, err := getopt.Getopts(args[1:], "aq")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -96,14 +77,14 @@ func Reply(aerc *widgets.Aerc, args []string) error {
|
||||||
}
|
}
|
||||||
if replyAll {
|
if replyAll {
|
||||||
for _, addr := range msg.Envelope.Cc {
|
for _, addr := range msg.Envelope.Cc {
|
||||||
cc = append(cc, formatAddress(addr))
|
cc = append(cc, lib.FormatAddress(addr))
|
||||||
}
|
}
|
||||||
for _, addr := range msg.Envelope.To {
|
for _, addr := range msg.Envelope.To {
|
||||||
address := fmt.Sprintf("%s@%s", addr.MailboxName, addr.HostName)
|
address := fmt.Sprintf("%s@%s", addr.MailboxName, addr.HostName)
|
||||||
if address == us.Address {
|
if address == us.Address {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
to = append(to, formatAddress(addr))
|
to = append(to, lib.FormatAddress(addr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,13 +7,13 @@
|
||||||
# with mutt's printf-like syntax. TODO: document properly
|
# with mutt's printf-like syntax. TODO: document properly
|
||||||
#
|
#
|
||||||
# Default:
|
# Default:
|
||||||
index-format=%4C %Z %D %-17.17n %s
|
index-format=%D %-17.17n %s
|
||||||
|
|
||||||
#
|
#
|
||||||
# See strftime(3)
|
# See time.Time#Format at https://godoc.org/time#Time.Format
|
||||||
#
|
#
|
||||||
# Default: %F %l:%M %p (ISO 8501 + 12 hour time)
|
# Default: 2006-01-02 03:04 PM (ISO 8601 + 12 hour time)
|
||||||
timestamp-format=%F %l:%M %p
|
timestamp-format=2006-01-02 03:04 PM
|
||||||
|
|
||||||
#
|
#
|
||||||
# Width of the sidebar, including the border.
|
# Width of the sidebar, including the border.
|
||||||
|
|
|
@ -246,8 +246,8 @@ func LoadConfig(root *string, sharedir string) (*AercConfig, error) {
|
||||||
Ini: file,
|
Ini: file,
|
||||||
|
|
||||||
Ui: UIConfig{
|
Ui: UIConfig{
|
||||||
IndexFormat: "%4C %Z %D %-17.17n %s",
|
IndexFormat: "%D %-17.17n %s",
|
||||||
TimestampFormat: "%F %l:%M %p",
|
TimestampFormat: "2006-01-02 03:04 PM",
|
||||||
ShowHeaders: []string{
|
ShowHeaders: []string{
|
||||||
"From", "To", "Cc", "Bcc", "Subject", "Date",
|
"From", "To", "Cc", "Bcc", "Subject", "Date",
|
||||||
},
|
},
|
||||||
|
|
|
@ -31,12 +31,12 @@ These options are configured in the *[ui]* section of aerc.conf.
|
||||||
Describes the format for each row in a mailbox view. This field is
|
Describes the format for each row in a mailbox view. This field is
|
||||||
compatible with mutt's printf-like syntax. TODO: document properly
|
compatible with mutt's printf-like syntax. TODO: document properly
|
||||||
|
|
||||||
Default: %4C %Z %D %-17.17n %s
|
Default: %D %-17.17n %s
|
||||||
|
|
||||||
*timestamp-format*
|
*timestamp-format*
|
||||||
See strftime(3)
|
See time.Time#Format at https://godoc.org/time#Time.Format
|
||||||
|
|
||||||
Default: %F %l:%M %p (ISO 8501 + 12 hour time)
|
Default: 2006-01-02 03:04 PM (ISO 8601 + 12 hour time)
|
||||||
|
|
||||||
*sidebar-width*
|
*sidebar-width*
|
||||||
Width of the sidebar, including the border. Set to zero to disable the
|
Width of the sidebar, including the border. Set to zero to disable the
|
||||||
|
|
40
lib/address.go
Normal file
40
lib/address.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
atom *regexp.Regexp = regexp.MustCompile("^[a-z0-9!#$%7'*+-/=?^_`{}|~ ]+$")
|
||||||
|
)
|
||||||
|
|
||||||
|
func FormatAddresses(addrs []*imap.Address) string {
|
||||||
|
val := bytes.Buffer{}
|
||||||
|
for i, addr := range addrs {
|
||||||
|
val.WriteString(FormatAddress(addr))
|
||||||
|
if i != len(addrs)-1 {
|
||||||
|
val.WriteString(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatAddress(addr *imap.Address) string {
|
||||||
|
if addr.PersonalName != "" {
|
||||||
|
if atom.MatchString(addr.PersonalName) {
|
||||||
|
return fmt.Sprintf("%s <%s@%s>",
|
||||||
|
addr.PersonalName, addr.MailboxName, addr.HostName)
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("\"%s\" <%s@%s>",
|
||||||
|
strings.ReplaceAll(addr.PersonalName, "\"", "'"),
|
||||||
|
addr.MailboxName, addr.HostName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("<%s@%s>", addr.MailboxName, addr.HostName)
|
||||||
|
}
|
||||||
|
}
|
231
lib/indexformat.go
Normal file
231
lib/indexformat.go
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
|
||||||
|
"git.sr.ht/~sircmpwn/aerc/config"
|
||||||
|
"git.sr.ht/~sircmpwn/aerc/worker/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseIndexFormat(conf *config.AercConfig, number int,
|
||||||
|
msg *types.MessageInfo) (string, []interface{}, error) {
|
||||||
|
|
||||||
|
format := conf.Ui.IndexFormat
|
||||||
|
retval := make([]byte, 0, len(format))
|
||||||
|
var args []interface{}
|
||||||
|
|
||||||
|
var c rune
|
||||||
|
for i, ni := 0, 0; i < len(format); {
|
||||||
|
ni = strings.IndexByte(format[i:], '%')
|
||||||
|
if ni < 0 {
|
||||||
|
ni = len(format)
|
||||||
|
retval = append(retval, []byte(format[i:ni])...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ni += i + 1
|
||||||
|
// Check for fmt flags
|
||||||
|
if ni == len(format) {
|
||||||
|
goto handle_end_error
|
||||||
|
}
|
||||||
|
c = rune(format[ni])
|
||||||
|
if c == '+' || c == '-' || c == '#' || c == ' ' || c == '0' {
|
||||||
|
ni++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for precision and width
|
||||||
|
if ni == len(format) {
|
||||||
|
goto handle_end_error
|
||||||
|
}
|
||||||
|
c = rune(format[ni])
|
||||||
|
for unicode.IsDigit(c) {
|
||||||
|
ni++
|
||||||
|
c = rune(format[ni])
|
||||||
|
}
|
||||||
|
if c == '.' {
|
||||||
|
ni++
|
||||||
|
c = rune(format[ni])
|
||||||
|
for unicode.IsDigit(c) {
|
||||||
|
ni++
|
||||||
|
c = rune(format[ni])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
retval = append(retval, []byte(format[i:ni])...)
|
||||||
|
// Get final format verb
|
||||||
|
if ni == len(format) {
|
||||||
|
goto handle_end_error
|
||||||
|
}
|
||||||
|
c = rune(format[ni])
|
||||||
|
switch c {
|
||||||
|
case '%':
|
||||||
|
retval = append(retval, '%')
|
||||||
|
case 'a':
|
||||||
|
if len(msg.Envelope.From) == 0 {
|
||||||
|
return "", nil, errors.New("found no address for sender")
|
||||||
|
}
|
||||||
|
addr := msg.Envelope.From[0]
|
||||||
|
retval = append(retval, 's')
|
||||||
|
args = append(args, fmt.Sprintf("%s@%s", addr.MailboxName,
|
||||||
|
addr.HostName))
|
||||||
|
case 'A':
|
||||||
|
var addr *imap.Address
|
||||||
|
if len(msg.Envelope.ReplyTo) == 0 {
|
||||||
|
if len(msg.Envelope.From) == 0 {
|
||||||
|
return "", nil,
|
||||||
|
errors.New("found no address for sender or reply-to")
|
||||||
|
} else {
|
||||||
|
addr = msg.Envelope.From[0]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addr = msg.Envelope.ReplyTo[0]
|
||||||
|
}
|
||||||
|
retval = append(retval, 's')
|
||||||
|
args = append(args, fmt.Sprintf("%s@%s", addr.MailboxName,
|
||||||
|
addr.HostName))
|
||||||
|
case 'C':
|
||||||
|
retval = append(retval, 'd')
|
||||||
|
args = append(args, number)
|
||||||
|
case 'd':
|
||||||
|
retval = append(retval, 's')
|
||||||
|
args = append(args, msg.InternalDate.Format(conf.Ui.TimestampFormat))
|
||||||
|
case 'D':
|
||||||
|
retval = append(retval, 's')
|
||||||
|
args = append(args, msg.InternalDate.Local().Format(conf.Ui.TimestampFormat))
|
||||||
|
case 'f':
|
||||||
|
if len(msg.Envelope.From) == 0 {
|
||||||
|
return "", nil, errors.New("found no address for sender")
|
||||||
|
}
|
||||||
|
addr := FormatAddress(msg.Envelope.From[0])
|
||||||
|
retval = append(retval, 's')
|
||||||
|
args = append(args, addr)
|
||||||
|
case 'F':
|
||||||
|
if len(msg.Envelope.From) == 0 {
|
||||||
|
return "", nil, errors.New("found no address for sender")
|
||||||
|
}
|
||||||
|
addr := msg.Envelope.From[0]
|
||||||
|
// TODO: handle case when sender is current user. Then
|
||||||
|
// use recipient's name
|
||||||
|
var val string
|
||||||
|
if addr.PersonalName != "" {
|
||||||
|
val = addr.PersonalName
|
||||||
|
} else {
|
||||||
|
val = fmt.Sprintf("%s@%s",
|
||||||
|
addr.MailboxName, addr.HostName)
|
||||||
|
}
|
||||||
|
retval = append(retval, 's')
|
||||||
|
args = append(args, val)
|
||||||
|
|
||||||
|
case 'i':
|
||||||
|
retval = append(retval, 's')
|
||||||
|
args = append(args, msg.Envelope.MessageId)
|
||||||
|
case 'n':
|
||||||
|
if len(msg.Envelope.From) == 0 {
|
||||||
|
return "", nil, errors.New("found no address for sender")
|
||||||
|
}
|
||||||
|
addr := msg.Envelope.From[0]
|
||||||
|
var val string
|
||||||
|
if addr.PersonalName != "" {
|
||||||
|
val = addr.PersonalName
|
||||||
|
} else {
|
||||||
|
val = fmt.Sprintf("%s@%s",
|
||||||
|
addr.MailboxName, addr.HostName)
|
||||||
|
}
|
||||||
|
retval = append(retval, 's')
|
||||||
|
args = append(args, val)
|
||||||
|
case 'r':
|
||||||
|
addrs := FormatAddresses(msg.Envelope.To)
|
||||||
|
retval = append(retval, 's')
|
||||||
|
args = append(args, addrs)
|
||||||
|
case 'R':
|
||||||
|
addrs := FormatAddresses(msg.Envelope.Cc)
|
||||||
|
retval = append(retval, 's')
|
||||||
|
args = append(args, addrs)
|
||||||
|
case 's':
|
||||||
|
retval = append(retval, 's')
|
||||||
|
args = append(args, msg.Envelope.Subject)
|
||||||
|
case 'u':
|
||||||
|
if len(msg.Envelope.From) == 0 {
|
||||||
|
return "", nil, errors.New("found no address for sender")
|
||||||
|
}
|
||||||
|
addr := msg.Envelope.From[0]
|
||||||
|
retval = append(retval, 's')
|
||||||
|
args = append(args, addr.MailboxName)
|
||||||
|
case 'v':
|
||||||
|
if len(msg.Envelope.From) == 0 {
|
||||||
|
return "", nil, errors.New("found no address for sender")
|
||||||
|
}
|
||||||
|
addr := msg.Envelope.From[0]
|
||||||
|
// check if message is from current user
|
||||||
|
if addr.PersonalName != "" {
|
||||||
|
retval = append(retval, 's')
|
||||||
|
args = append(args, strings.Split(addr.PersonalName, " ")[0])
|
||||||
|
}
|
||||||
|
case 'Z':
|
||||||
|
// calculate all flags
|
||||||
|
var readFlag = ""
|
||||||
|
var delFlag = ""
|
||||||
|
var flaggedFlag = ""
|
||||||
|
for _, flag := range msg.Flags {
|
||||||
|
if flag == "\\Seen" {
|
||||||
|
readFlag = "O" // message is old
|
||||||
|
} else if flag == "\\Recent" {
|
||||||
|
readFlag = "N" // message is new
|
||||||
|
} else if flag == "\\Answered" {
|
||||||
|
readFlag = "r" // message has been replied to
|
||||||
|
} else if flag == "\\Deleted" {
|
||||||
|
delFlag = "D"
|
||||||
|
// TODO: check if attachments
|
||||||
|
} else if flag == "\\Flagged" {
|
||||||
|
flaggedFlag = "!"
|
||||||
|
}
|
||||||
|
// TODO: check gpg stuff
|
||||||
|
}
|
||||||
|
retval = append(retval, '3', 's')
|
||||||
|
args = append(args, readFlag+delFlag+flaggedFlag)
|
||||||
|
|
||||||
|
// Move the below cases to proper alphabetical positions once
|
||||||
|
// implemented
|
||||||
|
case 'l':
|
||||||
|
// TODO: number of lines in the message
|
||||||
|
retval = append(retval, 'd')
|
||||||
|
args = append(args, msg.Size)
|
||||||
|
case 'e':
|
||||||
|
// TODO: current message number in thread
|
||||||
|
fallthrough
|
||||||
|
case 'E':
|
||||||
|
// TODO: number of messages in current thread
|
||||||
|
fallthrough
|
||||||
|
case 'H':
|
||||||
|
// TODO: spam attribute(s) of this message
|
||||||
|
fallthrough
|
||||||
|
case 'L':
|
||||||
|
// TODO:
|
||||||
|
fallthrough
|
||||||
|
case 'X':
|
||||||
|
// TODO: number of attachments
|
||||||
|
fallthrough
|
||||||
|
case 'y':
|
||||||
|
// TODO: X-Label field
|
||||||
|
fallthrough
|
||||||
|
case 'Y':
|
||||||
|
// TODO: X-Label field and some other constraints
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
// Just ignore it and print as is
|
||||||
|
// so %k in index format becomes %%k to Printf
|
||||||
|
retval = append(retval, '%')
|
||||||
|
retval = append(retval, byte(c))
|
||||||
|
}
|
||||||
|
i = ni + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(retval), args, nil
|
||||||
|
|
||||||
|
handle_end_error:
|
||||||
|
return "", nil, errors.New("reached end of string while parsing index format")
|
||||||
|
}
|
|
@ -80,7 +80,12 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
|
||||||
style = style.Foreground(tcell.ColorGray)
|
style = style.Foreground(tcell.ColorGray)
|
||||||
}
|
}
|
||||||
ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
|
ctx.Fill(0, row, ctx.Width(), 1, ' ', style)
|
||||||
ctx.Printf(0, row, style, "%s", msg.Envelope.Subject)
|
fmtStr, args, err := lib.ParseIndexFormat(ml.conf, i, msg)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Printf(0, row, style, "%v", err)
|
||||||
|
} else {
|
||||||
|
ctx.Printf(0, row, style, fmtStr, args...)
|
||||||
|
}
|
||||||
|
|
||||||
row += 1
|
row += 1
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package widgets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
@ -44,23 +43,6 @@ type PartSwitcher struct {
|
||||||
showHeaders bool
|
showHeaders bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatAddresses(addrs []*imap.Address) string {
|
|
||||||
val := bytes.Buffer{}
|
|
||||||
for i, addr := range addrs {
|
|
||||||
if addr.PersonalName != "" {
|
|
||||||
val.WriteString(fmt.Sprintf("%s <%s@%s>",
|
|
||||||
addr.PersonalName, addr.MailboxName, addr.HostName))
|
|
||||||
} else {
|
|
||||||
val.WriteString(fmt.Sprintf("%s@%s",
|
|
||||||
addr.MailboxName, addr.HostName))
|
|
||||||
}
|
|
||||||
if i != len(addrs)-1 {
|
|
||||||
val.WriteString(", ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return val.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
|
func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
|
||||||
store *lib.MessageStore, msg *types.MessageInfo) *MessageViewer {
|
store *lib.MessageStore, msg *types.MessageInfo) *MessageViewer {
|
||||||
|
|
||||||
|
@ -84,12 +66,12 @@ func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
|
||||||
headers.AddChild(
|
headers.AddChild(
|
||||||
&HeaderView{
|
&HeaderView{
|
||||||
Name: "From",
|
Name: "From",
|
||||||
Value: formatAddresses(msg.Envelope.From),
|
Value: lib.FormatAddresses(msg.Envelope.From),
|
||||||
}).At(0, 0)
|
}).At(0, 0)
|
||||||
headers.AddChild(
|
headers.AddChild(
|
||||||
&HeaderView{
|
&HeaderView{
|
||||||
Name: "To",
|
Name: "To",
|
||||||
Value: formatAddresses(msg.Envelope.To),
|
Value: lib.FormatAddresses(msg.Envelope.To),
|
||||||
}).At(0, 1)
|
}).At(0, 1)
|
||||||
headers.AddChild(
|
headers.AddChild(
|
||||||
&HeaderView{
|
&HeaderView{
|
||||||
|
@ -379,11 +361,11 @@ func NewPartViewer(conf *config.AercConfig,
|
||||||
case "subject":
|
case "subject":
|
||||||
header = msg.Envelope.Subject
|
header = msg.Envelope.Subject
|
||||||
case "from":
|
case "from":
|
||||||
header = formatAddresses(msg.Envelope.From)
|
header = lib.FormatAddresses(msg.Envelope.From)
|
||||||
case "to":
|
case "to":
|
||||||
header = formatAddresses(msg.Envelope.To)
|
header = lib.FormatAddresses(msg.Envelope.To)
|
||||||
case "cc":
|
case "cc":
|
||||||
header = formatAddresses(msg.Envelope.Cc)
|
header = lib.FormatAddresses(msg.Envelope.Cc)
|
||||||
}
|
}
|
||||||
if f.Regex.Match([]byte(header)) {
|
if f.Regex.Match([]byte(header)) {
|
||||||
filter = exec.Command("sh", "-c", f.Command)
|
filter = exec.Command("sh", "-c", f.Command)
|
||||||
|
|
Loading…
Reference in a new issue