Message list: implement index-format option

This commit is contained in:
Yash Srivastav 2019-06-08 01:05:23 +05:30 committed by Drew DeVault
parent 6d491569c0
commit fca7321639
8 changed files with 294 additions and 55 deletions

View file

@ -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))
} }
} }
} }

View file

@ -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.

View file

@ -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",
}, },

View file

@ -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
View 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
View 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")
}

View file

@ -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
} }

View file

@ -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)