From b82801500c46326585eaa992909e5bebc5c4f677 Mon Sep 17 00:00:00 2001 From: drew Date: Wed, 25 Mar 2026 15:28:32 +0400 Subject: [PATCH 1/2] feat: notifications Signed-off-by: drew --- main.go | 10 ++++++++++ notify/notify.go | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 notify/notify.go diff --git a/main.go b/main.go index d419726..b3cafac 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,7 @@ import ( "github.com/floatpane/matcha/clib" "github.com/floatpane/matcha/config" "github.com/floatpane/matcha/fetcher" + "github.com/floatpane/matcha/notify" "github.com/floatpane/matcha/plugin" "github.com/floatpane/matcha/sender" "github.com/floatpane/matcha/theme" @@ -485,6 +486,15 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, fetchFolderEmailsCmd(m.config, m.folderInbox.GetCurrentFolder()) case tui.IdleNewMailMsg: + // Send desktop notification for new mail + accountName := msg.AccountID + if m.config != nil { + if acc := m.config.GetAccountByID(msg.AccountID); acc != nil { + accountName = acc.Email + } + } + go notify.Send("Matcha", fmt.Sprintf("New mail in %s (%s)", msg.FolderName, accountName)) + // IDLE detected new mail — refetch the folder if we're viewing it if m.folderInbox != nil && m.folderInbox.GetCurrentFolder() == msg.FolderName { return m, tea.Batch( diff --git a/notify/notify.go b/notify/notify.go new file mode 100644 index 0000000..197db6d --- /dev/null +++ b/notify/notify.go @@ -0,0 +1,21 @@ +package notify + +import ( + "fmt" + "os/exec" + "runtime" +) + +// Send delivers a desktop notification with the given title and body. +// On macOS it uses osascript; on Linux it uses notify-send. +func Send(title, body string) error { + switch runtime.GOOS { + case "darwin": + script := fmt.Sprintf(`display notification %q with title %q sound name "default"`, body, title) + return exec.Command("osascript", "-e", script).Run() + case "linux": + return exec.Command("notify-send", title, body).Run() + default: + return nil + } +} From 8b1fa1fb4ffe70206dcc1a2838ec25973d2e8f93 Mon Sep 17 00:00:00 2001 From: drew Date: Wed, 25 Mar 2026 15:33:36 +0400 Subject: [PATCH 2/2] fix: add settings option to turn off/on Signed-off-by: drew --- config/config.go | 23 +++++++++++++---------- main.go | 14 ++++++++------ tui/settings.go | 29 ++++++++++++++++++++++++----- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/config/config.go b/config/config.go index 97e0fe5..a31a08e 100644 --- a/config/config.go +++ b/config/config.go @@ -46,11 +46,12 @@ type MailingList struct { // Config stores the user's email configuration with multiple accounts. type Config struct { - Accounts []Account `json:"accounts"` - DisableImages bool `json:"disable_images,omitempty"` - HideTips bool `json:"hide_tips,omitempty"` - Theme string `json:"theme,omitempty"` - MailingLists []MailingList `json:"mailing_lists,omitempty"` + Accounts []Account `json:"accounts"` + DisableImages bool `json:"disable_images,omitempty"` + HideTips bool `json:"hide_tips,omitempty"` + DisableNotifications bool `json:"disable_notifications,omitempty"` + Theme string `json:"theme,omitempty"` + MailingLists []MailingList `json:"mailing_lists,omitempty"` } // GetIMAPServer returns the IMAP server address for the account. @@ -192,11 +193,12 @@ func LoadConfig() (*Config, error) { AuthMethod string `json:"auth_method,omitempty"` } type diskConfig struct { - Accounts []rawAccount `json:"accounts"` - DisableImages bool `json:"disable_images,omitempty"` - HideTips bool `json:"hide_tips,omitempty"` - Theme string `json:"theme,omitempty"` - MailingLists []MailingList `json:"mailing_lists,omitempty"` + Accounts []rawAccount `json:"accounts"` + DisableImages bool `json:"disable_images,omitempty"` + HideTips bool `json:"hide_tips,omitempty"` + DisableNotifications bool `json:"disable_notifications,omitempty"` + Theme string `json:"theme,omitempty"` + MailingLists []MailingList `json:"mailing_lists,omitempty"` } var raw diskConfig @@ -226,6 +228,7 @@ func LoadConfig() (*Config, error) { config.DisableImages = raw.DisableImages config.HideTips = raw.HideTips + config.DisableNotifications = raw.DisableNotifications config.Theme = raw.Theme config.MailingLists = raw.MailingLists for _, rawAcc := range raw.Accounts { diff --git a/main.go b/main.go index b3cafac..986d390 100644 --- a/main.go +++ b/main.go @@ -486,14 +486,16 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, fetchFolderEmailsCmd(m.config, m.folderInbox.GetCurrentFolder()) case tui.IdleNewMailMsg: - // Send desktop notification for new mail - accountName := msg.AccountID - if m.config != nil { - if acc := m.config.GetAccountByID(msg.AccountID); acc != nil { - accountName = acc.Email + // Send desktop notification for new mail (if enabled) + if m.config == nil || !m.config.DisableNotifications { + accountName := msg.AccountID + if m.config != nil { + if acc := m.config.GetAccountByID(msg.AccountID); acc != nil { + accountName = acc.Email + } } + go notify.Send("Matcha", fmt.Sprintf("New mail in %s (%s)", msg.FolderName, accountName)) } - go notify.Send("Matcha", fmt.Sprintf("New mail in %s (%s)", msg.FolderName, accountName)) // IDLE detected new mail — refetch the folder if we're viewing it if m.folderInbox != nil && m.folderInbox.GetCurrentFolder() == msg.FolderName { diff --git a/tui/settings.go b/tui/settings.go index 5747021..18766b7 100644 --- a/tui/settings.go +++ b/tui/settings.go @@ -128,8 +128,8 @@ func (m *Settings) updateMain(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { m.cursor-- } case "down", "j": - // Options: 0: Email Accounts, 1: Theme, 2: Image Display, 3: Edit Signature, 4: Contextual Tips, 5: Mailing Lists - if m.cursor < 5 { + // Options: 0: Email Accounts, 1: Theme, 2: Image Display, 3: Edit Signature, 4: Contextual Tips, 5: Desktop Notifications, 6: Mailing Lists + if m.cursor < 6 { m.cursor++ } case "enter": @@ -160,7 +160,11 @@ func (m *Settings) updateMain(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { m.cfg.HideTips = !m.cfg.HideTips _ = config.SaveConfig(m.cfg) return m, nil - case 5: // Mailing Lists + case 5: // Desktop Notifications + m.cfg.DisableNotifications = !m.cfg.DisableNotifications + _ = config.SaveConfig(m.cfg) + return m, nil + case 6: // Mailing Lists m.state = SettingsMailingLists m.cursor = 0 return m, nil @@ -443,9 +447,22 @@ func (m *Settings) viewMain() string { } b.WriteString("\n") - // Option 5: Mailing Lists - mailingListsText := "Mailing Lists" + // Option 5: Desktop Notifications + notifStatus := "ON" + if m.cfg.DisableNotifications { + notifStatus = "OFF" + } + notifText := fmt.Sprintf("Desktop Notifications: %s", notifStatus) if m.cursor == 5 { + b.WriteString(selectedAccountItemStyle.Render("> " + notifText)) + } else { + b.WriteString(accountItemStyle.Render(" " + notifText)) + } + b.WriteString("\n") + + // Option 6: Mailing Lists + mailingListsText := "Mailing Lists" + if m.cursor == 6 { b.WriteString(selectedAccountItemStyle.Render("> " + mailingListsText)) } else { b.WriteString(accountItemStyle.Render(" " + mailingListsText)) @@ -466,6 +483,8 @@ func (m *Settings) viewMain() string { case 4: tip = "Toggle displaying helpful contextual tips like this one." case 5: + tip = "Toggle desktop notifications when new mail arrives." + case 6: tip = "Manage groups of email addresses to quickly send to multiple people." } if tip != "" {