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

@@ -53,5 +53,7 @@ VINYL = app_config.get("vinyl", {"enable": False})
BATTERY = app_config.get("battery", {"enable": False}) BATTERY = app_config.get("battery", {"enable": False})
WINDOW_TITLE = app_config.get("window_title", {"enable": True}) WINDOW_TITLE = app_config.get("window_title", {"enable": True})
STYLIX = app_config.get("stylix", {"enable": False}) 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) BAR_HEIGHT = app_config.get("height", 40)
DEV = app_config.get("dev", False) DEV = app_config.get("dev", False)

View File

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

View File

@@ -1,5 +1,6 @@
import json import json
import subprocess import subprocess
import shutil
from datetime import datetime from datetime import datetime
from fabric.widgets.box import Box from fabric.widgets.box import Box
from fabric.widgets.label import Label 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.image import Image
from fabric.widgets.wayland import WaylandWindow as Window from fabric.widgets.wayland import WaylandWindow as Window
from loguru import logger from loguru import logger
from bar.config import CALENDAR
class CalendarService: class CalendarService:
@@ -64,10 +66,27 @@ class CalendarService:
def update_events(self): def update_events(self):
"""Fetch today's events from khal""" """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: try:
result = subprocess.run( result = subprocess.run(
[ [
"khal", khal_path,
"list", "list",
"--json", "--json",
"title", "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")

View File

@@ -5,6 +5,7 @@
@import url("./bar.css"); @import url("./bar.css");
@import url("./finder.css"); @import url("./finder.css");
@import url("./calendar.css"); @import url("./calendar.css");
@import url("./notmuch.css");
/* unset so we can style everything from the ground up. */ /* unset so we can style everything from the ground up. */

35
bar/styles/notmuch.css Normal file
View File

@@ -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);
}

View File

@@ -6,6 +6,13 @@ vinyl:
enable: false enable: false
battery: battery:
enable: true 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: stylix:
enable: true enable: true
colors: colors: