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)