diff options
| -rw-r--r-- | aerc.go | 12 | ||||
| -rw-r--r-- | commands/commands.go | 6 | ||||
| -rw-r--r-- | config/aerc.conf.in | 11 | ||||
| -rw-r--r-- | config/config.go | 11 | ||||
| -rw-r--r-- | config/triggers.go | 49 | ||||
| -rw-r--r-- | doc/aerc-config.5.scd | 20 | ||||
| -rw-r--r-- | lib/format/format.go (renamed from lib/indexformat.go) | 58 | ||||
| -rw-r--r-- | lib/msgstore.go | 19 | ||||
| -rw-r--r-- | widgets/account.go | 6 | ||||
| -rw-r--r-- | widgets/aerc.go | 13 | ||||
| -rw-r--r-- | widgets/msglist.go | 5 | 
11 files changed, 175 insertions, 35 deletions
| @@ -52,7 +52,7 @@ func getCommands(selected libui.Drawable) []*commands.Commands {  	}  } -func execCommand(aerc *widgets.Aerc, ui *libui.UI, cmd string) error { +func execCommand(aerc *widgets.Aerc, ui *libui.UI, cmd []string) error {  	cmds := getCommands((*aerc).SelectedTab())  	for i, set := range cmds {  		err := set.ExecuteCommand(aerc, cmd) @@ -144,11 +144,11 @@ func main() {  		ui   *libui.UI  	) -	aerc = widgets.NewAerc(conf, logger, func(cmd string) error { -			return execCommand(aerc, ui, cmd) -		}, func(cmd string) []string { -			return getCompletions(aerc, cmd) -		}) +	aerc = widgets.NewAerc(conf, logger, func(cmd []string) error { +		return execCommand(aerc, ui, cmd) +	}, func(cmd string) []string { +		return getCompletions(aerc, cmd) +	})  	ui, err = libui.Initialize(conf, aerc)  	if err != nil { diff --git a/commands/commands.go b/commands/commands.go index 4038fe2..c6f149f 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -56,11 +56,7 @@ type CommandSource interface {  	Commands() *Commands  } -func (cmds *Commands) ExecuteCommand(aerc *widgets.Aerc, cmd string) error { -	args, err := shlex.Split(cmd) -	if err != nil { -		return err -	} +func (cmds *Commands) ExecuteCommand(aerc *widgets.Aerc, args []string) error {  	if len(args) == 0 {  		return errors.New("Expected a command.")  	} diff --git a/config/aerc.conf.in b/config/aerc.conf.in index 4219042..5b080e9 100644 --- a/config/aerc.conf.in +++ b/config/aerc.conf.in @@ -96,3 +96,14 @@ subject,~^\[PATCH=awk -f @SHAREDIR@/filters/hldiff  #text/html=@SHAREDIR@/filters/html  text/*=awk -f @SHAREDIR@/filters/plaintext  #image/*=catimg -w $(tput cols) - + +[triggers] +# +# Triggers specify commands to execute when certain events occur. +# +# Example: +# new-email=exec notify-send "New email from %n" "%s"<Enter> + +# +# Executed when a new email arrives in the selected folder +new-email= diff --git a/config/config.go b/config/config.go index 4a049fa..f863729 100644 --- a/config/config.go +++ b/config/config.go @@ -84,6 +84,11 @@ type ViewerConfig struct {  	HeaderLayout   [][]string `ini:"-"`  } +type TriggersConfig struct { +	NewEmail       string `ini:"new-email"` +	ExecuteCommand func(command []string) error +} +  type AercConfig struct {  	Bindings BindingConfig  	Compose  ComposeConfig @@ -91,6 +96,7 @@ type AercConfig struct {  	Accounts []AccountConfig `ini:"-"`  	Filters  []FilterConfig  `ini:"-"`  	Viewer   ViewerConfig    `ini:"-"` +	Triggers TriggersConfig  `ini:"-"`  	Ui       UIConfig  	General  GeneralConfig  } @@ -278,6 +284,11 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {  			return err  		}  	} +	if triggers, err := file.GetSection("triggers"); err == nil { +		if err := triggers.MapTo(&config.Triggers); err != nil { +			return err +		} +	}  	return nil  } diff --git a/config/triggers.go b/config/triggers.go new file mode 100644 index 0000000..d31f267 --- /dev/null +++ b/config/triggers.go @@ -0,0 +1,49 @@ +package config + +import ( +	"errors" +	"fmt" + +	"github.com/google/shlex" + +	"git.sr.ht/~sircmpwn/aerc/lib/format" +	"git.sr.ht/~sircmpwn/aerc/models" +) + +func (trig *TriggersConfig) ExecTrigger(triggerCmd string, +	triggerFmt func(string) (string, error)) error { + +	if len(triggerCmd) == 0 { +		return errors.New("Trigger command empty") +	} +	triggerCmdParts, err := shlex.Split(triggerCmd) +	if err != nil { +		return err +	} + +	var command []string +	for _, part := range triggerCmdParts { +		formattedPart, err := triggerFmt(part) +		if err != nil { +			return err +		} +		command = append(command, formattedPart) +	} +	return trig.ExecuteCommand(command) +} + +func (trig *TriggersConfig) ExecNewEmail(account *AccountConfig, +	conf *AercConfig, msg *models.MessageInfo) { +	err := trig.ExecTrigger(trig.NewEmail, +		func(part string) (string, error) { +			formatstr, args, err := format.ParseMessageFormat(part, +				conf.Ui.TimestampFormat, account.Name, 0, msg) +			if err != nil { +				return "", err +			} +			return fmt.Sprintf(formatstr, args...), nil +		}) +	if err != nil { +		fmt.Printf("Error from the new-email trigger: %s\n", err) +	} +} diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd index 665f4f4..08f65af 100644 --- a/doc/aerc-config.5.scd +++ b/doc/aerc-config.5.scd @@ -68,6 +68,10 @@ These options are configured in the *[ui]* section of aerc.conf.  :  comma-separated list of formatted CC names and addresses  |  %s  :  subject +|  %t +:  the (first) address the new email was sent to +|  %T +:  the account name which received the email  |  %u  :  sender mailbox name (e.g. "smith" in "smith@example.net")  |  %v @@ -164,6 +168,22 @@ aerc ships with some default filters installed in the share directory (usually  _/usr/share/aerc/filters_). Note that these may have additional dependencies  that aerc does not have alone. +## TRIGGERS + +Triggers specify commands to execute when certain events occur. + +They are configured in the *[triggers]* section of aerc.conf. + +*new-email* +	Executed when a new email arrives in the selected folder. + +	e.g. new-email=exec notify-send "New email from %n" "%s" + +	Default: "" + +	Format specifiers from *index-format* are expanded with respect to the new +	message. +  # ACCOUNTS.CONF  This file is used for configuring each mail account used for aerc. Each section diff --git a/lib/indexformat.go b/lib/format/format.go index 34b4d77..b403f2d 100644 --- a/lib/indexformat.go +++ b/lib/format/format.go @@ -1,4 +1,4 @@ -package lib +package format  import (  	"errors" @@ -6,14 +6,12 @@ import (  	"strings"  	"unicode" -	"git.sr.ht/~sircmpwn/aerc/config"  	"git.sr.ht/~sircmpwn/aerc/models"  ) -func ParseIndexFormat(conf *config.AercConfig, number int, -	msg *models.MessageInfo) (string, []interface{}, error) { - -	format := conf.Ui.IndexFormat +func ParseMessageFormat(format string, timestampformat string, +	accountName string, number int, msg *models.MessageInfo) (string, +	[]interface{}, error) {  	retval := make([]byte, 0, len(format))  	var args []interface{} @@ -64,11 +62,13 @@ func ParseIndexFormat(conf *config.AercConfig, number int,  			retval = append(retval, '%')  		case 'a':  			if len(msg.Envelope.From) == 0 { -				return "", nil, errors.New("found no address for sender") +				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.Mailbox, addr.Host)) +			args = append(args, +				fmt.Sprintf("%s@%s", addr.Mailbox, addr.Host))  		case 'A':  			var addr *models.Address  			if len(msg.Envelope.ReplyTo) == 0 { @@ -82,26 +82,31 @@ func ParseIndexFormat(conf *config.AercConfig, number int,  				addr = msg.Envelope.ReplyTo[0]  			}  			retval = append(retval, 's') -			args = append(args, fmt.Sprintf("%s@%s", addr.Mailbox, addr.Host)) +			args = append(args, +				fmt.Sprintf("%s@%s", addr.Mailbox, addr.Host))  		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)) +			args = append(args, +				msg.InternalDate.Format(timestampformat))  		case 'D':  			retval = append(retval, 's') -			args = append(args, msg.InternalDate.Local().Format(conf.Ui.TimestampFormat)) +			args = append(args, +				msg.InternalDate.Local().Format(timestampformat))  		case 'f':  			if len(msg.Envelope.From) == 0 { -				return "", nil, errors.New("found no address for sender") +				return "", nil, +					errors.New("found no address for sender")  			}  			addr := msg.Envelope.From[0].Format()  			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") +				return "", nil, +					errors.New("found no address for sender")  			}  			addr := msg.Envelope.From[0]  			// TODO: handle case when sender is current user. Then @@ -120,7 +125,8 @@ func ParseIndexFormat(conf *config.AercConfig, number int,  			args = append(args, msg.Envelope.MessageId)  		case 'n':  			if len(msg.Envelope.From) == 0 { -				return "", nil, errors.New("found no address for sender") +				return "", nil, +					errors.New("found no address for sender")  			}  			addr := msg.Envelope.From[0]  			var val string @@ -142,22 +148,37 @@ func ParseIndexFormat(conf *config.AercConfig, number int,  		case 's':  			retval = append(retval, 's')  			args = append(args, msg.Envelope.Subject) +		case 't': +			if len(msg.Envelope.To) == 0 { +				return "", nil, +					errors.New("found no address for recipient") +			} +			addr := msg.Envelope.To[0] +			retval = append(retval, 's') +			args = append(args, +				fmt.Sprintf("%s@%s", addr.Mailbox, addr.Host)) +		case 'T': +			retval = append(retval, 's') +			args = append(args, accountName)  		case 'u':  			if len(msg.Envelope.From) == 0 { -				return "", nil, errors.New("found no address for sender") +				return "", nil, +					errors.New("found no address for sender")  			}  			addr := msg.Envelope.From[0]  			retval = append(retval, 's')  			args = append(args, addr.Mailbox)  		case 'v':  			if len(msg.Envelope.From) == 0 { -				return "", nil, errors.New("found no address for sender") +				return "", nil, +					errors.New("found no address for sender")  			}  			addr := msg.Envelope.From[0]  			// check if message is from current user  			if addr.Name != "" {  				retval = append(retval, 's') -				args = append(args, strings.Split(addr.Name, " ")[0]) +				args = append(args, +					strings.Split(addr.Name, " ")[0])  			}  		case 'Z':  			// calculate all flags @@ -237,5 +258,6 @@ func ParseIndexFormat(conf *config.AercConfig, number int,  	return string(retval), args, nil  handle_end_error: -	return "", nil, errors.New("reached end of string while parsing index format") +	return "", nil, +		errors.New("reached end of string while parsing message format")  } diff --git a/lib/msgstore.go b/lib/msgstore.go index 736217e..53faaac 100644 --- a/lib/msgstore.go +++ b/lib/msgstore.go @@ -32,10 +32,13 @@ type MessageStore struct {  	pendingBodies  map[uint32]interface{}  	pendingHeaders map[uint32]interface{}  	worker         *types.Worker + +	triggerNewEmail func(*models.MessageInfo)  }  func NewMessageStore(worker *types.Worker, -	dirInfo *models.DirectoryInfo) *MessageStore { +	dirInfo *models.DirectoryInfo, +	triggerNewEmail func(*models.MessageInfo)) *MessageStore {  	return &MessageStore{  		Deleted: make(map[uint32]interface{}), @@ -48,6 +51,8 @@ func NewMessageStore(worker *types.Worker,  		pendingBodies:  make(map[uint32]interface{}),  		pendingHeaders: make(map[uint32]interface{}),  		worker:         worker, + +		triggerNewEmail: triggerNewEmail,  	}  } @@ -165,6 +170,18 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {  		} else {  			store.Messages[msg.Info.Uid] = msg.Info  		} +		seen := false +		recent := false +		for _, flag := range msg.Info.Flags { +			if flag == models.RecentFlag { +				recent = true +			} else if flag == models.SeenFlag { +				seen = true +			} +		} +		if !seen && recent { +			store.triggerNewEmail(msg.Info) +		}  		if _, ok := store.pendingHeaders[msg.Info.Uid]; msg.Info.Envelope != nil && ok {  			delete(store.pendingHeaders, msg.Info.Uid)  			if cbs, ok := store.headerCallbacks[msg.Info.Uid]; ok { diff --git a/widgets/account.go b/widgets/account.go index f070df1..92e7a56 100644 --- a/widgets/account.go +++ b/widgets/account.go @@ -203,7 +203,11 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {  		if store, ok := acct.msgStores[msg.Info.Name]; ok {  			store.Update(msg)  		} else { -			store = lib.NewMessageStore(acct.worker, msg.Info) +			store = lib.NewMessageStore(acct.worker, msg.Info, +				func(msg *models.MessageInfo) { +					acct.conf.Triggers.ExecNewEmail(acct.acct, +						acct.conf, msg) +				})  			acct.msgStores[msg.Info.Name] = store  			store.OnUpdate(func(_ *lib.MessageStore) {  				store.OnUpdate(nil) diff --git a/widgets/aerc.go b/widgets/aerc.go index 079d442..3cf1f64 100644 --- a/widgets/aerc.go +++ b/widgets/aerc.go @@ -8,6 +8,7 @@ import (  	"time"  	"github.com/gdamore/tcell" +	"github.com/google/shlex"  	"git.sr.ht/~sircmpwn/aerc/config"  	"git.sr.ht/~sircmpwn/aerc/lib/ui" @@ -16,7 +17,7 @@ import (  type Aerc struct {  	accounts    map[string]*AccountView -	cmd         func(cmd string) error +	cmd         func(cmd []string) error  	complete    func(cmd string) []string  	conf        *config.AercConfig  	focused     libui.Interactive @@ -30,7 +31,7 @@ type Aerc struct {  }  func NewAerc(conf *config.AercConfig, logger *log.Logger, -	cmd func(cmd string) error, complete func(cmd string) []string) *Aerc { +	cmd func(cmd []string) error, complete func(cmd string) []string) *Aerc {  	tabs := libui.NewTabs() @@ -62,6 +63,7 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,  	}  	statusline.SetAerc(aerc) +	conf.Triggers.ExecuteCommand = cmd  	for i, acct := range conf.Accounts {  		view := NewAccountView(conf, &conf.Accounts[i], logger, aerc) @@ -311,7 +313,12 @@ func (aerc *Aerc) focus(item libui.Interactive) {  func (aerc *Aerc) BeginExCommand() {  	previous := aerc.focused  	exline := NewExLine(func(cmd string) { -		err := aerc.cmd(cmd) +		parts, err := shlex.Split(cmd) +		if err != nil { +			aerc.PushStatus(" "+err.Error(), 10*time.Second). +				Color(tcell.ColorDefault, tcell.ColorRed) +		} +		err = aerc.cmd(parts)  		if err != nil {  			aerc.PushStatus(" "+err.Error(), 10*time.Second).  				Color(tcell.ColorDefault, tcell.ColorRed) diff --git a/widgets/msglist.go b/widgets/msglist.go index e8ba8c1..abf6921 100644 --- a/widgets/msglist.go +++ b/widgets/msglist.go @@ -9,6 +9,7 @@ import (  	"git.sr.ht/~sircmpwn/aerc/config"  	"git.sr.ht/~sircmpwn/aerc/lib" +	"git.sr.ht/~sircmpwn/aerc/lib/format"  	"git.sr.ht/~sircmpwn/aerc/lib/ui"  	"git.sr.ht/~sircmpwn/aerc/models"  ) @@ -95,7 +96,9 @@ func (ml *MessageList) Draw(ctx *ui.Context) {  		}  		ctx.Fill(0, row, ctx.Width(), 1, ' ', style) -		fmtStr, args, err := lib.ParseIndexFormat(ml.conf, i, msg) +		fmtStr, args, err := format.ParseMessageFormat( +			ml.conf.Ui.IndexFormat, +			ml.conf.Ui.TimestampFormat, "", i, msg)  		if err != nil {  			ctx.Printf(0, row, style, "%v", err)  		} else { | 
