about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--commands/msg/reply.go25
-rw-r--r--config/aerc.conf.in8
-rw-r--r--config/config.go4
-rw-r--r--doc/aerc-config.5.scd6
-rw-r--r--lib/address.go40
-rw-r--r--lib/indexformat.go231
-rw-r--r--widgets/msglist.go7
-rw-r--r--widgets/msgviewer.go28
8 files changed, 294 insertions, 55 deletions
diff --git a/commands/msg/reply.go b/commands/msg/reply.go
index e09a118..a9ae5a1 100644
--- a/commands/msg/reply.go
+++ b/commands/msg/reply.go
@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"io"
 	gomail "net/mail"
-	"regexp"
 	"strings"
 
 	"git.sr.ht/~sircmpwn/getopt"
@@ -15,6 +14,7 @@ import (
 	_ "github.com/emersion/go-message/charset"
 	"github.com/emersion/go-message/mail"
 
+	"git.sr.ht/~sircmpwn/aerc/lib"
 	"git.sr.ht/~sircmpwn/aerc/widgets"
 )
 
@@ -23,25 +23,6 @@ func init() {
 	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 {
 	opts, optind, err := getopt.Getopts(args[1:], "aq")
 	if err != nil {
@@ -96,14 +77,14 @@ func Reply(aerc *widgets.Aerc, args []string) error {
 		}
 		if replyAll {
 			for _, addr := range msg.Envelope.Cc {
-				cc = append(cc, formatAddress(addr))
+				cc = append(cc, lib.FormatAddress(addr))
 			}
 			for _, addr := range msg.Envelope.To {
 				address := fmt.Sprintf("%s@%s", addr.MailboxName, addr.HostName)
 				if address == us.Address {
 					continue
 				}
-				to = append(to, formatAddress(addr))
+				to = append(to, lib.FormatAddress(addr))
 			}
 		}
 	}
diff --git a/config/aerc.conf.in b/config/aerc.conf.in
index 1a4a826..de1c3ec 100644
--- a/config/aerc.conf.in
+++ b/config/aerc.conf.in
@@ -7,13 +7,13 @@
 # with mutt's printf-like syntax. TODO: document properly
 #
 # 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)
-timestamp-format=%F %l:%M %p
+# Default: 2006-01-02 03:04 PM (ISO 8601 + 12 hour time)
+timestamp-format=2006-01-02 03:04 PM
 
 #
 # Width of the sidebar, including the border.
diff --git a/config/config.go b/config/config.go
index 3b7edbb..3ef587b 100644
--- a/config/config.go
+++ b/config/config.go
@@ -246,8 +246,8 @@ func LoadConfig(root *string, sharedir string) (*AercConfig, error) {
 		Ini: file,
 
 		Ui: UIConfig{
-			IndexFormat:     "%4C %Z %D %-17.17n %s",
-			TimestampFormat: "%F %l:%M %p",
+			IndexFormat:     "%D %-17.17n %s",
+			TimestampFormat: "2006-01-02 03:04 PM",
 			ShowHeaders: []string{
 				"From", "To", "Cc", "Bcc", "Subject", "Date",
 			},
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index 409e863..e002764 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -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
 	compatible with mutt's printf-like syntax. TODO: document properly
 
-	Default: %4C %Z %D %-17.17n %s
+	Default: %D %-17.17n %s
 
 *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*
 	Width of the sidebar, including the border. Set to zero to disable the
diff --git a/lib/address.go b/lib/address.go
new file mode 100644
index 0000000..b557195
--- /dev/null
+++ b/lib/address.go
@@ -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)
+	}
+}
diff --git a/lib/indexformat.go b/lib/indexformat.go
new file mode 100644
index 0000000..3e139e6
--- /dev/null
+++ b/lib/indexformat.go
@@ -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")
+}
diff --git a/widgets/msglist.go b/widgets/msglist.go
index caa868f..f1cbb31 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -80,7 +80,12 @@ func (ml *MessageList) Draw(ctx *ui.Context) {
 			style = style.Foreground(tcell.ColorGray)
 		}
 		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
 	}
diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go
index 6017e50..52407b7 100644
--- a/widgets/msgviewer.go
+++ b/widgets/msgviewer.go
@@ -2,7 +2,6 @@ package widgets
 
 import (
 	"bufio"
-	"bytes"
 	"fmt"
 	"io"
 	"os/exec"
@@ -44,23 +43,6 @@ type PartSwitcher struct {
 	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,
 	store *lib.MessageStore, msg *types.MessageInfo) *MessageViewer {
 
@@ -84,12 +66,12 @@ func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
 	headers.AddChild(
 		&HeaderView{
 			Name:  "From",
-			Value: formatAddresses(msg.Envelope.From),
+			Value: lib.FormatAddresses(msg.Envelope.From),
 		}).At(0, 0)
 	headers.AddChild(
 		&HeaderView{
 			Name:  "To",
-			Value: formatAddresses(msg.Envelope.To),
+			Value: lib.FormatAddresses(msg.Envelope.To),
 		}).At(0, 1)
 	headers.AddChild(
 		&HeaderView{
@@ -379,11 +361,11 @@ func NewPartViewer(conf *config.AercConfig,
 			case "subject":
 				header = msg.Envelope.Subject
 			case "from":
-				header = formatAddresses(msg.Envelope.From)
+				header = lib.FormatAddresses(msg.Envelope.From)
 			case "to":
-				header = formatAddresses(msg.Envelope.To)
+				header = lib.FormatAddresses(msg.Envelope.To)
 			case "cc":
-				header = formatAddresses(msg.Envelope.Cc)
+				header = lib.FormatAddresses(msg.Envelope.Cc)
 			}
 			if f.Regex.Match([]byte(header)) {
 				filter = exec.Command("sh", "-c", f.Command)