init: pristine aerc 0.20.0 source

This commit is contained in:
Mortdecai
2026-04-07 19:54:54 -04:00
commit 083402a548
502 changed files with 68722 additions and 0 deletions
+408
View File
@@ -0,0 +1,408 @@
package templates
import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"text/template"
"time"
"git.sr.ht/~rjarry/aerc/lib/format"
"git.sr.ht/~rjarry/aerc/lib/parse"
"git.sr.ht/~rjarry/aerc/models"
"github.com/emersion/go-message/mail"
)
var version string
// SetVersion initializes the aerc version displayed in template functions
func SetVersion(v string) {
version = v
}
var execPath string
func SetExecPath(dirs []string) {
// prepend aerc filters dirs to the default exec path
paths := make([]string, 0, len(dirs)+1)
for _, d := range dirs {
paths = append(paths, filepath.Join(d, "filters"))
}
paths = append(paths, os.Getenv("PATH"))
execPath = strings.Join(paths, ":")
}
// wrap allows to chain wrapText
func wrap(lineWidth int, text string) string {
return wrapText(text, lineWidth)
}
func wrapLine(text string, lineWidth int) string {
words := strings.Fields(text)
if len(words) == 0 {
return text
}
var wrapped strings.Builder
wrapped.WriteString(words[0])
spaceLeft := lineWidth - wrapped.Len()
for _, word := range words[1:] {
if len(word)+1 > spaceLeft {
wrapped.WriteRune('\n')
wrapped.WriteString(word)
spaceLeft = lineWidth - len(word)
} else {
wrapped.WriteRune(' ')
wrapped.WriteString(word)
spaceLeft -= 1 + len(word)
}
}
return wrapped.String()
}
func wrapText(text string, lineWidth int) string {
text = strings.ReplaceAll(text, "\r\n", "\n")
text = strings.TrimRight(text, "\n")
lines := strings.Split(text, "\n")
var wrapped strings.Builder
for _, line := range lines {
switch {
case line == "":
// deliberately left blank
case line[0] == '>':
// leave quoted text alone
wrapped.WriteString(line)
default:
wrapped.WriteString(wrapLine(line, lineWidth))
}
wrapped.WriteRune('\n')
}
return wrapped.String()
}
// quote prepends "> " in front of every line in text
func quote(text string) string {
text = strings.ReplaceAll(text, "\r\n", "\n")
text = strings.TrimRight(text, "\n")
lines := strings.Split(text, "\n")
var quoted strings.Builder
for _, line := range lines {
if line == "" {
quoted.WriteString(">\n")
continue
}
if strings.HasPrefix(line, ">") {
quoted.WriteString(">")
} else {
quoted.WriteString("> ")
}
quoted.WriteString(line)
quoted.WriteRune('\n')
}
return quoted.String()
}
// cmd allow to parse reply by shell command
// text have to be passed by cmd param
// if there is error, original string is returned
func cmd(cmd, text string) string {
var out bytes.Buffer
c := exec.Command("sh", "-c", cmd)
c.Env = append(os.Environ(), "PATH="+execPath)
c.Stdin = strings.NewReader(text)
c.Stdout = &out
err := c.Run()
if err != nil {
return text
}
return out.String()
}
func toLocal(t time.Time) time.Time {
return time.Time.In(t, time.Local)
}
func rearrangeNameWithComma(name string) string {
parts := strings.SplitN(name, ",", 3)
if len(parts) == 2 {
return fmt.Sprintf("%s %s", strings.TrimSpace(parts[1]), strings.TrimSpace(parts[0]))
}
return name
}
func names(addresses []*mail.Address) []string {
n := make([]string, len(addresses))
for i, addr := range addresses {
name := rearrangeNameWithComma(addr.Name)
if name == "" {
parts := strings.SplitN(addr.Address, "@", 2)
name = parts[0]
}
n[i] = name
}
return n
}
func firstnames(addresses []*mail.Address) []string {
n := make([]string, len(addresses))
for i, addr := range addresses {
var name string
if addr.Name == "" {
parts := strings.SplitN(addr.Address, "@", 2)
parts = strings.SplitN(parts[0], ".", 2)
name = parts[0]
} else {
name = rearrangeNameWithComma(addr.Name)
name = strings.SplitN(name, " ", 2)[0] // split by spaces and get the first word
name = strings.SplitN(name, ",", 2)[0] // split by commas and get the first word
}
n[i] = name
}
return n
}
func initials(addresses []*mail.Address) []string {
n := names(addresses)
ret := make([]string, len(addresses))
for i, name := range n {
split := strings.Split(name, " ")
initial := ""
for _, s := range split {
initial += string([]rune(s)[0:1])
}
ret[i] = initial
}
return ret
}
func emails(addresses []*mail.Address) []string {
e := make([]string, len(addresses))
for i, addr := range addresses {
e[i] = addr.Address
}
return e
}
func mboxes(addresses []*mail.Address) []string {
e := make([]string, len(addresses))
for i, addr := range addresses {
parts := strings.SplitN(addr.Address, "@", 2)
e[i] = parts[0]
}
return e
}
func shortmboxes(addresses []*mail.Address) []string {
e := make([]string, len(addresses))
for i, addr := range addresses {
parts := strings.SplitN(addr.Address, "@", 2)
parts = strings.SplitN(parts[0], ".", 2)
e[i] = parts[0]
}
return e
}
func persons(addresses []*mail.Address) []string {
e := make([]string, len(addresses))
for i, addr := range addresses {
e[i] = format.AddressForHumans(addr)
}
return e
}
var units = []string{"K", "M", "G", "T"}
func humanReadable(value int) string {
sign := ""
if value < 0 {
sign = "-"
value = -value
}
if value < 1000 {
return fmt.Sprintf("%s%d", sign, value)
}
val := float64(value)
unit := ""
for i := 0; val >= 1000 && i < len(units); i++ {
unit = units[i]
val /= 1000.0
}
if val < 100.0 {
return fmt.Sprintf("%s%.1f%s", sign, val, unit)
}
return fmt.Sprintf("%s%.0f%s", sign, val, unit)
}
func cwd() string {
path, err := os.Getwd()
if err != nil {
return err.Error()
}
home, err := os.UserHomeDir()
if err != nil {
return err.Error()
}
if strings.HasPrefix(path, home) {
path = strings.Replace(path, home, "~", 1)
}
return path
}
func join(sep string, elems []string) string {
return strings.Join(elems, sep)
}
func split(sep string, s string) []string {
return strings.Split(s, sep)
}
// removes a signature from the piped in message
func trimSignature(message string) string {
var res strings.Builder
input := bufio.NewScanner(strings.NewReader(message))
for input.Scan() {
line := input.Text()
if line == "-- " {
break
}
res.WriteString(line)
res.WriteRune('\n')
}
return res.String()
}
func compactDir(path string) string {
return format.CompactPath(path, os.PathSeparator)
}
type (
Case struct{ expr, value string }
Default struct{ value string }
Exclude struct{ expr string }
)
func (c *Case) Matches(s string) bool { return parse.MatchCache(s, c.expr) }
func (c *Case) Value() string { return c.value }
func (c *Case) Skip() bool { return false }
func (d *Default) Matches(s string) bool { return true }
func (d *Default) Value() string { return d.value }
func (d *Default) Skip() bool { return false }
func (e *Exclude) Matches(s string) bool { return parse.MatchCache(s, e.expr) }
func (e *Exclude) Value() string { return "" }
func (e *Exclude) Skip() bool { return true }
func switch_(value string, cases ...models.Case) string {
for _, c := range cases {
if c.Matches(value) {
return c.Value()
}
}
return ""
}
func case_(expr, value string) models.Case {
return &Case{expr: expr, value: value}
}
func default_(value string) models.Case {
return &Default{value: value}
}
func exclude(expr string) models.Case {
return &Exclude{expr: expr}
}
func map_(elements []string, cases ...models.Case) []string {
mapped := make([]string, 0, len(elements))
top:
for _, e := range elements {
for _, c := range cases {
if c.Matches(e) {
if c.Skip() {
continue top
}
e = c.Value()
break
}
}
mapped = append(mapped, e)
}
return mapped
}
func replace(pattern, subst, value string) string {
re := regexp.MustCompile(pattern)
return re.ReplaceAllString(value, subst)
}
func contains(substring, s string) bool {
return strings.Contains(s, substring)
}
func hasPrefix(prefix, s string) bool {
return strings.HasPrefix(s, prefix)
}
func head(n uint, s string) string {
r := []rune(s)
length := uint(len(r))
if length >= n {
return string(r[:n])
}
return s
}
func tail(n uint, s string) string {
r := []rune(s)
length := uint(len(r))
if length >= n {
return string(r[length-n:])
}
return s
}
var templateFuncs = template.FuncMap{
"quote": quote,
"wrapText": wrapText,
"wrap": wrap,
"now": time.Now,
"dateFormat": time.Time.Format,
"toLocal": toLocal,
"exec": cmd,
"version": func() string { return version },
"names": names,
"firstnames": firstnames,
"initials": initials,
"emails": emails,
"mboxes": mboxes,
"shortmboxes": shortmboxes,
"persons": persons,
"humanReadable": humanReadable,
"cwd": cwd,
"join": join,
"split": split,
"trimSignature": trimSignature,
"compactDir": compactDir,
"match": parse.MatchCache,
"switch": switch_,
"case": case_,
"default": default_,
"map": map_,
"exclude": exclude,
"contains": contains,
"hasPrefix": hasPrefix,
"toLower": strings.ToLower,
"toUpper": strings.ToUpper,
"replace": replace,
"head": head,
"tail": tail,
}
+157
View File
@@ -0,0 +1,157 @@
package templates
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/emersion/go-message/mail"
)
func TestTemplates_DifferentNamesFormats(t *testing.T) {
type testCase struct {
address mail.Address
name string
}
cases := []testCase{
{address: mail.Address{Name: "", Address: "john@doe.com"}, name: "john"},
{address: mail.Address{Name: "", Address: "bill.john.doe@doe.com"}, name: "bill.john.doe"},
{address: mail.Address{Name: "John", Address: "john@doe.com"}, name: "John"},
{address: mail.Address{Name: "John Doe", Address: "john@doe.com"}, name: "John Doe"},
{address: mail.Address{Name: "Bill John Doe", Address: "john@doe.com"}, name: "Bill John Doe"},
{address: mail.Address{Name: "Doe, John", Address: "john@doe.com"}, name: "John Doe"},
{address: mail.Address{Name: "Doe, Bill John", Address: "john@doe.com"}, name: "Bill John Doe"},
{address: mail.Address{Name: "Schröder, Gerhard", Address: "s@g.de"}, name: "Gerhard Schröder"},
{address: mail.Address{Name: "Buhl-Freiherr von und zu Guttenberg, Karl-Theodor Maria Nikolaus Johann Jacob Philipp Franz Joseph Sylvester", Address: "long@email.com"}, name: "Karl-Theodor Maria Nikolaus Johann Jacob Philipp Franz Joseph Sylvester Buhl-Freiherr von und zu Guttenberg"},
{address: mail.Address{Name: "Dr. Őz-Szűcs Villő, MD, PhD, MBA (Üllői úti Klinika, Budapest, Hungary)", Address: "a@b.com"}, name: "Dr. Őz-Szűcs Villő, MD, PhD, MBA (Üllői úti Klinika, Budapest, Hungary)"},
{address: mail.Address{Name: "International Important Conference, 2023", Address: "a@b.com"}, name: "2023 International Important Conference"},
{address: mail.Address{Name: "A. B.C. Muscat", Address: "a@b.com"}, name: "A. B.C. Muscat"},
{address: mail.Address{Name: "Wertram, te, K.W.", Address: "a@b.com"}, name: "Wertram, te, K.W."},
{address: mail.Address{Name: "Harvard, John, Dr. CDC/MIT/SYSOPSYS", Address: "a@b.com"}, name: "Harvard, John, Dr. CDC/MIT/SYSOPSYS"},
}
for _, c := range cases {
names := names([]*mail.Address{&c.address})
assert.Len(t, names, 1)
assert.Equal(t, c.name, names[0])
}
}
func TestTemplates_DifferentFirstnamesFormats(t *testing.T) {
type testCase struct {
address mail.Address
firstname string
}
cases := []testCase{
{address: mail.Address{Name: "", Address: "john@doe.com"}, firstname: "john"},
{address: mail.Address{Name: "", Address: "bill.john.doe@doe.com"}, firstname: "bill"},
{address: mail.Address{Name: "John", Address: "john@doe.com"}, firstname: "John"},
{address: mail.Address{Name: "John Doe", Address: "john@doe.com"}, firstname: "John"},
{address: mail.Address{Name: "Bill John Doe", Address: "john@doe.com"}, firstname: "Bill"},
{address: mail.Address{Name: "Doe, John", Address: "john@doe.com"}, firstname: "John"},
{address: mail.Address{Name: "Schröder, Gerhard", Address: "s@g.de"}, firstname: "Gerhard"},
{address: mail.Address{Name: "Buhl-Freiherr von und zu Guttenberg, Karl-Theodor Maria Nikolaus Johann Jacob Philipp Franz Joseph Sylvester", Address: "long@email.com"}, firstname: "Karl-Theodor"},
{address: mail.Address{Name: "Dr. Őz-Szűcs Villő, MD, PhD, MBA (Üllői úti Klinika, Budapest, Hungary)", Address: "a@b.com"}, firstname: "Dr."},
{address: mail.Address{Name: "International Important Conference, 2023", Address: "a@b.com"}, firstname: "2023"},
{address: mail.Address{Name: "A. B.C. Muscat", Address: "a@b.com"}, firstname: "A."},
{address: mail.Address{Name: "Wertram, te, K.W.", Address: "a@b.com"}, firstname: "Wertram"},
{address: mail.Address{Name: "Harvard, John, Dr. CDC/MIT/SYSOPSYS", Address: "a@b.com"}, firstname: "Harvard"},
}
for _, c := range cases {
names := firstnames([]*mail.Address{&c.address})
assert.Len(t, names, 1)
assert.Equal(t, c.firstname, names[0])
}
}
func TestTemplates_InternalRearrangeNamesWithComma(t *testing.T) {
type testCase struct {
source string
res string
}
cases := []testCase{
{source: "John.Doe", res: "John.Doe"},
{source: "John Doe", res: "John Doe"},
{source: "John Bill Doe", res: "John Bill Doe"},
{source: "Doe, John Bill", res: "John Bill Doe"},
{source: "Doe, John-Bill", res: "John-Bill Doe"},
{source: "Doe John, Bill", res: "Bill Doe John"},
{source: "Schröder, Gerhard", res: "Gerhard Schröder"},
// do not touch names with more than one comma
{source: "One, Two, Three", res: "One, Two, Three"},
{source: "One, Two, Three, Four", res: "One, Two, Three, Four"},
}
for _, c := range cases {
res := rearrangeNameWithComma(c.source)
assert.Equal(t, c.res, res)
}
}
func TestTemplates_DifferentInitialsFormats(t *testing.T) {
type testCase struct {
address mail.Address
initials string
}
cases := []testCase{
{address: mail.Address{Name: "", Address: "john@doe.com"}, initials: "j"},
{address: mail.Address{Name: "", Address: "bill.john.doe@doe.com"}, initials: "b"},
{address: mail.Address{Name: "John", Address: "john@doe.com"}, initials: "J"},
{address: mail.Address{Name: "John Doe", Address: "john@doe.com"}, initials: "JD"},
{address: mail.Address{Name: "Bill John Doe", Address: "john@doe.com"}, initials: "BJD"},
{address: mail.Address{Name: "Doe, John", Address: "john@doe.com"}, initials: "JD"},
{address: mail.Address{Name: "Doe, John Bill", Address: "john@doe.com"}, initials: "JBD"},
{address: mail.Address{Name: "Schröder, Gerhard", Address: "s@g.de"}, initials: "GS"},
{address: mail.Address{Name: "Buhl-Freiherr von und zu Guttenberg, Karl-Theodor Maria Nikolaus Johann Jacob Philipp Franz Joseph Sylvester", Address: "long@email.com"}, initials: "KMNJJPFJSBvuzG"},
{address: mail.Address{Name: "Dr. Őz-Szűcs Villő, MD, PhD, MBA (Üllői úti Klinika, Budapest, Hungary)", Address: "a@b.com"}, initials: "DŐVMPM(úKBH"},
{address: mail.Address{Name: "International Important Conference, 2023", Address: "a@b.com"}, initials: "2IIC"},
{address: mail.Address{Name: "A. B.C. Muscat", Address: "a@b.com"}, initials: "ABM"},
{address: mail.Address{Name: "Wertram, te, K.W.", Address: "a@b.com"}, initials: "WtK"},
{address: mail.Address{Name: "Harvard, John, Dr. CDC/MIT/SYSOPSYS", Address: "a@b.com"}, initials: "HJDC"},
}
for _, c := range cases {
intls := initials([]*mail.Address{&c.address})
assert.Len(t, intls, 1)
assert.Equal(t, c.initials, intls[0])
}
}
func TestTemplates_Head(t *testing.T) {
type testCase struct {
head uint
input string
output string
}
cases := []testCase{
{head: 3, input: "abcde", output: "abc"},
{head: 10, input: "abcde", output: "abcde"},
}
for _, c := range cases {
out := head(c.head, c.input)
assert.Equal(t, c.output, out)
}
}
func TestTemplates_Tail(t *testing.T) {
type testCase struct {
tail uint
input string
output string
}
cases := []testCase{
{tail: 2, input: "abcde", output: "de"},
{tail: 8, input: "abcde", output: "abcde"},
}
for _, c := range cases {
out := tail(c.tail, c.input)
assert.Equal(t, c.output, out)
}
}
+105
View File
@@ -0,0 +1,105 @@
package templates
import (
"bytes"
"fmt"
"io"
"os"
"reflect"
"text/template"
"git.sr.ht/~rjarry/aerc/lib/xdg"
"git.sr.ht/~rjarry/aerc/models"
)
func findTemplate(templateName string, templateDirs []string) (string, error) {
for _, dir := range templateDirs {
templateFile := xdg.ExpandHome(dir, templateName)
if _, err := os.Stat(templateFile); os.IsNotExist(err) {
continue
}
return templateFile, nil
}
return "", fmt.Errorf(
"Can't find template %q in any of %v ", templateName, templateDirs)
}
func ParseTemplateFromFile(
name string, dirs []string, data models.TemplateData,
) (io.Reader, error) {
templateFile, err := findTemplate(name, dirs)
if err != nil {
return nil, err
}
emailTemplate, err := template.New(name).
Funcs(templateFuncs).ParseFiles(templateFile)
if err != nil {
return nil, err
}
var body bytes.Buffer
if err := Render(emailTemplate, &body, data); err != nil {
return nil, err
}
return &body, nil
}
func ParseTemplate(name, content string) (*template.Template, error) {
return template.New(name).Funcs(templateFuncs).Parse(content)
}
func Render(t *template.Template, w io.Writer, data models.TemplateData) error {
return t.Execute(w, data)
}
// builtins is a slice of keywords and functions built into the Go standard
// library for templates. Since they are not exported, they are hardcoded here.
var builtins = []string{
// from the Go standard library: src/text/template/parse/lex.go
"block",
"break",
"continue",
"define",
"else",
"end",
"if",
"range",
"nil",
"template",
"with",
// from the Go standard library: src/text/template/funcs.go
"and",
"call",
"html",
"index",
"slice",
"js",
"len",
"not",
"or",
"print",
"printf",
"println",
"urlquery",
"eq",
"ge",
"gt",
"le",
"lt",
"ne",
}
func Terms() []string {
var s []string
t := reflect.TypeOf((*models.TemplateData)(nil)).Elem()
for i := 0; i < t.NumMethod(); i++ {
s = append(s, "."+t.Method(i).Name)
}
for fnStr := range templateFuncs {
s = append(s, fnStr)
}
s = append(s, builtins...)
return s
}