From 15077fe6fa580121eb0c6160e3f7dbf07c7d792a Mon Sep 17 00:00:00 2001 From: Makesesama Date: Mon, 29 Sep 2025 23:24:23 +0200 Subject: [PATCH] feat: mail --- bar/config.py | 2 + bar/modules/bar.py | 10 ++- bar/modules/calendar.py | 21 ++++- bar/modules/notmuch.py | 168 ++++++++++++++++++++++++++++++++++++++++ bar/styles/main.css | 1 + bar/styles/notmuch.css | 35 +++++++++ example-stylix-dev.yaml | 7 ++ 7 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 bar/modules/notmuch.py create mode 100644 bar/styles/notmuch.css diff --git a/bar/config.py b/bar/config.py index 90fe190..28fd5fc 100644 --- a/bar/config.py +++ b/bar/config.py @@ -53,5 +53,7 @@ VINYL = app_config.get("vinyl", {"enable": False}) BATTERY = app_config.get("battery", {"enable": False}) WINDOW_TITLE = app_config.get("window_title", {"enable": True}) STYLIX = app_config.get("stylix", {"enable": False}) +CALENDAR = app_config.get("calendar", {"enable": True, "khal_path": "khal"}) +NOTMUCH = app_config.get("notmuch", {"enable": True, "notmuch_path": "notmuch", "emacsclient_command": "emacsclient"}) BAR_HEIGHT = app_config.get("height", 40) DEV = app_config.get("dev", False) diff --git a/bar/modules/bar.py b/bar/modules/bar.py index 42cc0a8..d0ce867 100644 --- a/bar/modules/bar.py +++ b/bar/modules/bar.py @@ -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 = [] diff --git a/bar/modules/calendar.py b/bar/modules/calendar.py index 1c18940..f17930f 100644 --- a/bar/modules/calendar.py +++ b/bar/modules/calendar.py @@ -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", diff --git a/bar/modules/notmuch.py b/bar/modules/notmuch.py new file mode 100644 index 0000000..ee50d60 --- /dev/null +++ b/bar/modules/notmuch.py @@ -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") \ No newline at end of file diff --git a/bar/styles/main.css b/bar/styles/main.css index e3dc704..43f884e 100644 --- a/bar/styles/main.css +++ b/bar/styles/main.css @@ -5,6 +5,7 @@ @import url("./bar.css"); @import url("./finder.css"); @import url("./calendar.css"); +@import url("./notmuch.css"); /* unset so we can style everything from the ground up. */ diff --git a/bar/styles/notmuch.css b/bar/styles/notmuch.css new file mode 100644 index 0000000..a4ce92b --- /dev/null +++ b/bar/styles/notmuch.css @@ -0,0 +1,35 @@ +/* Notmuch email widget styling */ + +#notmuch-widget { + background-color: var(--module-bg); + padding: 4px 8px; + border-radius: 12px; + transition: background-color 0.2s ease; +} + +#notmuch-widget:hover { + background-color: var(--light-bg); +} + +#notmuch-widget.has-unread { + background-color: var(--blue); +} + +#notmuch-widget.has-unread:hover { + background-color: var(--turquoise); +} + +#notmuch-widget.no-unread { + background-color: var(--module-bg); +} + +#unread-count { + color: var(--foreground); + font-size: 14px; + font-weight: bold; + min-width: 16px; +} + +#notmuch-widget.has-unread #unread-count { + color: var(--background); +} \ No newline at end of file diff --git a/example-stylix-dev.yaml b/example-stylix-dev.yaml index f110d04..732d739 100644 --- a/example-stylix-dev.yaml +++ b/example-stylix-dev.yaml @@ -6,6 +6,13 @@ vinyl: enable: false battery: enable: true +calendar: + enable: true + khal_path: "khal" # or full path like "/home/user/.nix-profile/bin/khal" +notmuch: + enable: true + notmuch_path: "notmuch" # or full path like "/home/user/.nix-profile/bin/notmuch" + emacsclient_command: "emacsclient" # or full path like "/home/user/.nix-profile/bin/emacsclient" stylix: enable: true colors: