feat: mail

This commit is contained in:
2025-09-29 23:24:23 +02:00
parent 34e837562f
commit 15077fe6fa
7 changed files with 242 additions and 2 deletions

View File

@@ -9,6 +9,7 @@ from bar.modules.player import Player
from bar.modules.vinyl import VinylButton
from bar.modules.battery import Battery
from bar.modules.calendar import CalendarService, CalendarPopup
from bar.modules.notmuch import NotmuchWidget
from fabric.widgets.wayland import WaylandWindow as Window
from fabric.system_tray.widgets import SystemTray
from fabric.river.widgets import (
@@ -20,7 +21,7 @@ from fabric.river.widgets import (
from fabric.widgets.circularprogressbar import CircularProgressBar
from bar.services.system_stats import SystemStatsService
from bar.config import VINYL, BATTERY, BAR_HEIGHT, WINDOW_TITLE
from bar.config import VINYL, BATTERY, BAR_HEIGHT, WINDOW_TITLE, NOTMUCH
class StatusBar(Window):
@@ -101,6 +102,10 @@ class StatusBar(Window):
if BATTERY["enable"]:
self.battery = Battery()
self.notmuch = None
if NOTMUCH["enable"]:
self.notmuch = NotmuchWidget()
self.status_container = Box(
name="widgets-container",
spacing=4,
@@ -120,6 +125,9 @@ class StatusBar(Window):
if self.battery:
end_container_children.append(self.battery)
if self.notmuch:
end_container_children.append(self.notmuch)
end_container_children.append(self.date_time)
center_children = []

View File

@@ -1,5 +1,6 @@
import json
import subprocess
import shutil
from datetime import datetime
from fabric.widgets.box import Box
from fabric.widgets.label import Label
@@ -7,6 +8,7 @@ from fabric.widgets.button import Button
from fabric.widgets.image import Image
from fabric.widgets.wayland import WaylandWindow as Window
from loguru import logger
from bar.config import CALENDAR
class CalendarService:
@@ -64,10 +66,27 @@ class CalendarService:
def update_events(self):
"""Fetch today's events from khal"""
# Check if calendar is enabled
if not CALENDAR.get("enable", True):
logger.info("[Calendar] Calendar is disabled in config")
self.events = []
self.emit_events_changed(self.events)
return
# Get khal path from config
khal_path = CALENDAR.get("khal_path", "khal")
# Check if khal is available
if not shutil.which(khal_path):
logger.warning(f"[Calendar] khal not found at '{khal_path}'. Please install khal or configure the correct path.")
self.events = []
self.emit_events_changed(self.events)
return
try:
result = subprocess.run(
[
"khal",
khal_path,
"list",
"--json",
"title",

168
bar/modules/notmuch.py Normal file
View File

@@ -0,0 +1,168 @@
import subprocess
import shutil
from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.button import Button
from fabric.widgets.image import Image
from loguru import logger
from bar.config import NOTMUCH
class NotmuchService:
def __init__(self, update_interval=60000): # 1 minute default
self.unread_count = 0
self.callbacks = []
self._update_interval = update_interval
self._timer_id = None
# Initial load
self.update_unread_count()
# Start periodic updates
self.start_monitoring()
def connect(self, signal_name, callback):
"""Simple callback system to replace signals"""
if signal_name == "unread-changed":
self.callbacks.append(callback)
def emit_unread_changed(self, count):
"""Emit unread changed to all callbacks"""
for callback in self.callbacks:
callback(self, count)
def start_monitoring(self):
"""Start periodic unread count updates"""
if self._timer_id is None:
from fabric.utils import invoke_repeater
self._timer_id = invoke_repeater(
self._update_interval, self._periodic_update
)
logger.info(
f"[Notmuch] Started periodic updates every {self._update_interval/1000} seconds"
)
def stop_monitoring(self):
"""Stop periodic unread count updates"""
if self._timer_id is not None:
from gi.repository import GLib
GLib.source_remove(self._timer_id)
self._timer_id = None
logger.info("[Notmuch] Stopped periodic updates")
def _periodic_update(self):
"""Periodic update callback"""
logger.info("[Notmuch] Performing periodic unread count update")
self.update_unread_count()
return True # Keep the timer running
def get_cached_count(self):
"""Get cached unread count without triggering update"""
return self.unread_count
def update_unread_count(self):
"""Fetch unread email count from notmuch"""
# Check if notmuch is enabled
if not NOTMUCH.get("enable", True):
logger.info("[Notmuch] Notmuch is disabled in config")
self.unread_count = 0
self.emit_unread_changed(self.unread_count)
return
# Get notmuch path from config
notmuch_path = NOTMUCH.get("notmuch_path", "notmuch")
# Check if notmuch is available
if not shutil.which(notmuch_path):
logger.warning(f"[Notmuch] notmuch not found at '{notmuch_path}'. Please install notmuch or configure the correct path.")
self.unread_count = 0
self.emit_unread_changed(self.unread_count)
return
try:
# Get unread email count
result = subprocess.run(
[notmuch_path, "count", "tag:unread"],
capture_output=True,
text=True,
check=True,
)
if result.stdout.strip():
self.unread_count = int(result.stdout.strip())
logger.info(f"[Notmuch] Found {self.unread_count} unread emails")
self.emit_unread_changed(self.unread_count)
else:
self.unread_count = 0
self.emit_unread_changed(self.unread_count)
except subprocess.CalledProcessError as e:
logger.error(f"[Notmuch] Failed to fetch unread count: {e}")
self.unread_count = 0
self.emit_unread_changed(self.unread_count)
except ValueError as e:
logger.error(f"[Notmuch] Error parsing unread count: {e}")
self.unread_count = 0
self.emit_unread_changed(self.unread_count)
except Exception as e:
logger.error(f"[Notmuch] Error getting unread count: {e}")
self.unread_count = 0
self.emit_unread_changed(self.unread_count)
class NotmuchWidget(Button):
def __init__(self, **kwargs):
# Create the widget content
self.icon = Image(icon_name="mail-unread-symbolic", icon_size=16)
self.label = Label("0", name="unread-count")
# Container for icon and label
container = Box(
orientation="h",
spacing=4,
children=[self.icon, self.label]
)
super().__init__(
name="notmuch-widget",
child=container,
on_clicked=self.open_email_client,
**kwargs,
)
# Initialize the service
self.service = NotmuchService()
self.service.connect("unread-changed", self.update_display)
logger.info("[Notmuch] Notmuch widget initialized")
# Initial update
self.update_display(self.service, self.service.unread_count)
def open_email_client(self, button=None):
"""Open notmuch in emacsclient"""
emacsclient_command = NOTMUCH.get("emacsclient_command", "emacsclient")
try:
# Open emacsclient with notmuch function
subprocess.Popen([emacsclient_command, "-c", "-e", "(notmuch)"], start_new_session=True)
logger.info(f"[Notmuch] Opened notmuch in emacsclient with command: {emacsclient_command}")
except Exception as e:
logger.error(f"[Notmuch] Failed to open notmuch in emacsclient '{emacsclient_command}': {e}")
def update_display(self, service, count):
"""Update the widget display with unread count"""
# Only show count if there are unread emails
if count > 0:
self.label.set_text(str(count))
self.label.set_visible(True)
self.icon.set_from_icon_name("mail-unread-symbolic", 16)
self.set_style_classes(["notmuch-widget", "has-unread"])
else:
self.label.set_text("")
self.label.set_visible(False)
self.icon.set_from_icon_name("mail-read-symbolic", 16)
self.set_style_classes(["notmuch-widget", "no-unread"])
logger.info(f"[Notmuch] Updated display: {count} unread emails")