152f8c9519ac1b7b35c3789b03f3d1cc3b1e8881 — Ben Burwell 3 months ago 2804f00 0.2.0
Ring bell when new messages arrive

Add a "new-message-bell" option to the UI section of aerc.conf. A new
hook into the message store allows the msglist widget to detect new
messages being added to the displayed list. When new messages are
delivered, and the new-message-bell option is enabled (as it is by
default), the terminal will beep.
M config/aerc.conf.in => config/aerc.conf.in +6 -0
@@ 37,6 37,12 @@ empty-dirlist=(no folders)
 # Default: false
 mouse-enabled=false
 
+#
+# Ring the bell when new messages are received
+#
+# Default: yes
+new-message-bell=true
+
 [viewer]
 #
 # Specifies the pager to use when displaying emails. Note that some filters

M config/config.go => config/config.go +2 -0
@@ 32,6 32,7 @@ type UIConfig struct {
 	EmptyMessage      string   `ini:"empty-message"`
 	EmptyDirlist      string   `ini:"empty-dirlist"`
 	MouseEnabled      bool     `ini:"mouse-enabled"`
+	NewMessageBell    bool     `ini:"new-message-bell"`
 }
 
 const (


@@ 344,6 345,7 @@ func LoadConfigFromFile(root *string, sharedir string) (*AercConfig, error) {
 			EmptyMessage:      "(no messages)",
 			EmptyDirlist:      "(no folders)",
 			MouseEnabled:      false,
+			NewMessageBell:    true,
 		},
 
 		Viewer: ViewerConfig{

M doc/aerc-config.5.scd => doc/aerc-config.5.scd +5 -0
@@ 105,6 105,11 @@ These options are configured in the *[ui]* section of aerc.conf.
 
 	Default: false
 
+*new-message-bell*
+	Ring the bell when a new message is received.
+
+	Default: true
+
 ## VIEWER
 
 These options are configured in the *[viewer]* section of aerc.conf.

M lib/msgstore.go => lib/msgstore.go +12 -3
@@ 33,12 33,14 @@ type MessageStore struct {
 	pendingHeaders map[uint32]interface{}
 	worker         *types.Worker
 
-	triggerNewEmail func(*models.MessageInfo)
+	triggerNewEmail        func(*models.MessageInfo)
+	triggerDirectoryChange func()
 }
 
 func NewMessageStore(worker *types.Worker,
 	dirInfo *models.DirectoryInfo,
-	triggerNewEmail func(*models.MessageInfo)) *MessageStore {
+	triggerNewEmail func(*models.MessageInfo),
+	triggerDirectoryChange func()) *MessageStore {
 
 	return &MessageStore{
 		Deleted: make(map[uint32]interface{}),


@@ 52,7 54,8 @@ func NewMessageStore(worker *types.Worker,
 		pendingHeaders: make(map[uint32]interface{}),
 		worker:         worker,
 
-		triggerNewEmail: triggerNewEmail,
+		triggerNewEmail:        triggerNewEmail,
+		triggerDirectoryChange: triggerDirectoryChange,
 	}
 }
 


@@ 147,6 150,7 @@ func merge(to *models.MessageInfo, from *models.MessageInfo) {
 
 func (store *MessageStore) Update(msg types.WorkerMessage) {
 	update := false
+	directoryChange := false
 	switch msg := msg.(type) {
 	case *types.DirectoryInfo:
 		store.DirInfo = *msg.Info


@@ 159,6 163,7 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
 				newMap[uid] = msg
 			} else {
 				newMap[uid] = nil
+				directoryChange = true
 			}
 		}
 		store.Messages = newMap


@@ 225,6 230,10 @@ func (store *MessageStore) Update(msg types.WorkerMessage) {
 	if update {
 		store.update()
 	}
+
+	if directoryChange && store.triggerDirectoryChange != nil {
+		store.triggerDirectoryChange()
+	}
 }
 
 func (store *MessageStore) OnUpdate(fn func(store *MessageStore)) {

M lib/ui/interfaces.go => lib/ui/interfaces.go +9 -0
@@ 23,6 23,10 @@ type Interactive interface {
 	Focus(focus bool)
 }
 
+type Beeper interface {
+	OnBeep(func() error)
+}
+
 type Simulator interface {
 	// Queues up the given input events for simulation
 	Simulate(events []tcell.Event)


@@ 33,6 37,11 @@ type DrawableInteractive interface {
 	Interactive
 }
 
+type DrawableInteractiveBeeper interface {
+	DrawableInteractive
+	Beeper
+}
+
 // A drawable which contains other drawables
 type Container interface {
 	Drawable

M lib/ui/ui.go => lib/ui/ui.go +2 -1
@@ 19,7 19,7 @@ type UI struct {
 }
 
 func Initialize(conf *config.AercConfig,
-	content DrawableInteractive) (*UI, error) {
+	content DrawableInteractiveBeeper) (*UI, error) {
 
 	screen, err := tcell.NewScreen()
 	if err != nil {


@@ 57,6 57,7 @@ func Initialize(conf *config.AercConfig,
 	content.OnInvalidate(func(_ Drawable) {
 		atomic.StoreInt32(&state.invalid, 1)
 	})
+	content.OnBeep(screen.Beep)
 	content.Focus(true)
 
 	return &state, nil

M widgets/account.go => widgets/account.go +4 -0
@@ 205,6 205,10 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) {
 				func(msg *models.MessageInfo) {
 					acct.conf.Triggers.ExecNewEmail(acct.acct,
 						acct.conf, msg)
+				}, func() {
+					if acct.conf.Ui.NewMessageBell {
+						acct.host.Beep()
+					}
 				})
 			acct.dirlist.SetMsgStore(msg.Info.Name, store)
 			store.OnUpdate(func(_ *lib.MessageStore) {

M widgets/aerc.go => widgets/aerc.go +15 -0
@@ 30,6 30,7 @@ type Aerc struct {
 	statusline  *StatusLine
 	pendingKeys []config.KeyStroke
 	tabs        *libui.Tabs
+	beep        func() error
 }
 
 func NewAerc(conf *config.AercConfig, logger *log.Logger,


@@ 84,6 85,20 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger,
 	return aerc
 }
 
+func (aerc *Aerc) OnBeep(f func() error) {
+	aerc.beep = f
+}
+
+func (aerc *Aerc) Beep() {
+	if aerc.beep == nil {
+		aerc.logger.Printf("should beep, but no beeper")
+		return
+	}
+	if err := aerc.beep(); err != nil {
+		aerc.logger.Printf("tried to beep, but could not: %v", err)
+	}
+}
+
 func (aerc *Aerc) Tick() bool {
 	more := false
 	for _, acct := range aerc.accounts {

M widgets/tabhost.go => widgets/tabhost.go +1 -0
@@ 8,4 8,5 @@ type TabHost interface {
 	BeginExCommand()
 	SetStatus(status string) *StatusMessage
 	PushStatus(text string, expiry time.Duration) *StatusMessage
+	Beep()
 }