feat: email debt timer

This commit is contained in:
2026-05-06 21:22:27 +02:00
parent 4adace6c4c
commit cfdcf0c039
4 changed files with 114 additions and 48 deletions

View File

@@ -131,6 +131,21 @@
default = "emacsclient";
description = "Path to the emacsclient binary";
};
debt_query = lib.mkOption {
type = lib.types.str;
default = "tag:unread and date:..1w";
description = "notmuch query whose count drives the mail-debt severity color on the bar widget";
};
debt_warn_at = lib.mkOption {
type = lib.types.int;
default = 1;
description = "Debt count at which the widget switches to the warn (orange) color";
};
debt_alarm_at = lib.mkOption {
type = lib.types.int;
default = 6;
description = "Debt count at which the widget switches to the alarm (red) color";
};
};
screenrec = {
enable = lib.mkOption {
@@ -210,6 +225,9 @@
enable = true;
notmuch_path = "notmuch";
emacsclient_command = "emacsclient";
debt_query = "tag:unread and date:..1w";
debt_warn_at = 1;
debt_alarm_at = 6;
};
screenrec = {
enable = false;

View File

@@ -131,7 +131,7 @@ def open_screenshot_menu():
@Application.action()
def refresh_notmuch():
if notmuch_widget is not None:
notmuch_widget.service.update_unread_count()
notmuch_widget.service.update_counts()
@Application.action()

View File

@@ -12,27 +12,33 @@ from loguru import logger
from sims.config import NOTMUCH
DEFAULT_DEBT_QUERY = "tag:unread and date:..1w"
DEFAULT_DEBT_WARN_AT = 1
DEFAULT_DEBT_ALARM_AT = 6
class NotmuchService:
def __init__(self, update_interval=60000): # 1 minute default
self.unread_count = 0
self.debt_count = 0
self.callbacks = []
self._update_interval = update_interval
self._timer_id = None
# Initial load
self.update_unread_count()
self.update_counts()
# Start periodic updates
self.start_monitoring()
def connect(self, signal_name, callback):
"""Simple callback system to replace signals"""
if signal_name == "unread-changed":
if signal_name == "counts-changed":
self.callbacks.append(callback)
def emit_unread_changed(self, count):
"""Emit unread changed to all callbacks"""
def emit_counts_changed(self):
"""Emit counts changed to all callbacks"""
for callback in self.callbacks:
callback(self, count)
callback(self, self.unread_count, self.debt_count)
def start_monitoring(self):
"""Start periodic unread count updates"""
@@ -57,21 +63,38 @@ class NotmuchService:
def _periodic_update(self):
"""Periodic update callback"""
logger.info("[Notmuch] Performing periodic unread count update")
self.update_unread_count()
logger.info("[Notmuch] Performing periodic count update")
self.update_counts()
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"""
def get_cached_debt_count(self):
"""Get cached debt count without triggering update"""
return self.debt_count
def _run_count(self, notmuch_path, query):
cmd = [notmuch_path, "count", query]
logger.info(f"[Notmuch] Running command: {' '.join(cmd)}")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True,
)
out = result.stdout.strip()
return int(out) if out else 0
def update_counts(self):
"""Fetch unread + debt counts 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)
self.debt_count = 0
self.emit_counts_changed()
return
# Get notmuch path from config
@@ -81,42 +104,34 @@ class NotmuchService:
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)
self.debt_count = 0
self.emit_counts_changed()
return
debt_query = NOTMUCH.get("debt_query", DEFAULT_DEBT_QUERY)
try:
# Get unread email count
cmd = [notmuch_path, "count", "tag:unread"]
logger.info(f"[Notmuch] Running command: {' '.join(cmd)}")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True,
self.unread_count = self._run_count(notmuch_path, "tag:unread")
self.debt_count = self._run_count(notmuch_path, debt_query)
logger.info(
f"[Notmuch] {self.unread_count} unread, {self.debt_count} aging (debt query: {debt_query!r})"
)
logger.info(f"[Notmuch] Command stdout: '{result.stdout.strip()}'")
logger.info(f"[Notmuch] Command stderr: '{result.stderr.strip()}'")
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)
self.emit_counts_changed()
except subprocess.CalledProcessError as e:
logger.error(f"[Notmuch] Failed to fetch unread count: {e}")
logger.error(f"[Notmuch] Failed to fetch counts: {e}")
self.unread_count = 0
self.emit_unread_changed(self.unread_count)
self.debt_count = 0
self.emit_counts_changed()
except ValueError as e:
logger.error(f"[Notmuch] Error parsing unread count: {e}")
logger.error(f"[Notmuch] Error parsing count: {e}")
self.unread_count = 0
self.emit_unread_changed(self.unread_count)
self.debt_count = 0
self.emit_counts_changed()
except Exception as e:
logger.error(f"[Notmuch] Error getting unread count: {e}")
logger.error(f"[Notmuch] Error getting counts: {e}")
self.unread_count = 0
self.emit_unread_changed(self.unread_count)
self.debt_count = 0
self.emit_counts_changed()
class NotmuchWidget(Button):
@@ -141,12 +156,14 @@ class NotmuchWidget(Button):
# Initialize the service
self.service = NotmuchService()
self.service.connect("unread-changed", self.update_display)
self.service.connect("counts-changed", self.update_display)
logger.info("[Notmuch] Notmuch widget initialized")
# Initial update
self.update_display(self.service, self.service.unread_count)
self.update_display(
self.service, self.service.unread_count, self.service.debt_count
)
def open_email_client(self, button=None):
"""Open notmuch in emacsclient"""
@@ -161,18 +178,31 @@ class NotmuchWidget(Button):
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))
def update_display(self, service, unread, debt):
"""Update the widget display with unread + debt counts"""
warn_at = NOTMUCH.get("debt_warn_at", DEFAULT_DEBT_WARN_AT)
alarm_at = NOTMUCH.get("debt_alarm_at", DEFAULT_DEBT_ALARM_AT)
classes = ["notmuch-widget"]
if unread > 0:
self.label.set_text(str(unread))
self.label.set_visible(True)
self.icon.set_property("icon-name", "mail-unread-symbolic")
self.set_style_classes(["notmuch-widget", "has-unread"])
classes.append("has-unread")
else:
self.label.set_text("")
self.label.set_visible(False)
self.icon.set_property("icon-name", "mail-read-symbolic")
self.set_style_classes(["notmuch-widget", "no-unread"])
classes.append("no-unread")
logger.info(f"[Notmuch] Updated display: {count} unread emails")
if debt >= alarm_at:
classes.append("debt-alarm")
elif debt >= warn_at:
classes.append("debt-warn")
self.set_style_classes(classes)
self.set_tooltip_text(f"{unread} unread · {debt} aging")
logger.info(
f"[Notmuch] Updated display: {unread} unread, {debt} aging — classes={classes}"
)

View File

@@ -23,6 +23,22 @@
background-color: var(--module-bg);
}
#notmuch-widget.debt-warn {
background-color: var(--orange);
}
#notmuch-widget.debt-warn:hover {
background-color: var(--gold);
}
#notmuch-widget.debt-alarm {
background-color: var(--red);
}
#notmuch-widget.debt-alarm:hover {
background-color: var(--pink);
}
#unread-count {
color: var(--foreground);
font-size: 14px;
@@ -30,6 +46,8 @@
min-width: 16px;
}
#notmuch-widget.has-unread #unread-count {
#notmuch-widget.has-unread #unread-count,
#notmuch-widget.debt-warn #unread-count,
#notmuch-widget.debt-alarm #unread-count {
color: var(--background);
}