diff --git a/bar/modules/calendar.py b/bar/modules/calendar.py index f17930f..f5f9447 100644 --- a/bar/modules/calendar.py +++ b/bar/modules/calendar.py @@ -1,7 +1,7 @@ import json import subprocess import shutil -from datetime import datetime +from datetime import datetime, date from fabric.widgets.box import Box from fabric.widgets.label import Label from fabric.widgets.button import Button @@ -10,6 +10,17 @@ from fabric.widgets.wayland import WaylandWindow as Window from loguru import logger from bar.config import CALENDAR +# Try to import khal as a Python library +try: + from khal.cli.main import main_khal + from khal.settings import get_config + from khal.khalendar import CalendarCollection + KHAL_AVAILABLE = True + logger.info("[Calendar] Using khal as Python library") +except ImportError: + KHAL_AVAILABLE = False + logger.info("[Calendar] khal Python library not available, falling back to subprocess") + class CalendarService: def __init__(self, update_interval=300000): # 5 minutes default @@ -64,15 +75,52 @@ class CalendarService: """Get cached events without triggering update""" return self.events - 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 + def update_events_python_api(self): + """Fetch today's events using khal Python API""" + try: + # Get khal configuration + config = get_config() + # Create calendar collection + collection = CalendarCollection.from_calendars( + calendars=config['calendars'], + dbpath=config['sqlite']['path'], + locale=config['locale'], + color=config['default']['print_new'], + unicode_symbols=config['default']['unicode_symbols'], + default_calendar=config['default']['default_calendar'], + readonly=True + ) + + # Get today's events + today = date.today() + events = collection.get_events_on(today) + + # Format events to match our expected structure + formatted_events = [] + for event in events: + formatted_event = { + 'title': str(event.summary), + 'start': event.start.strftime('%m-%d %H:%M') if hasattr(event.start, 'strftime') else '', + 'end': event.end.strftime('%m-%d %H:%M') if hasattr(event.end, 'strftime') else '', + 'location': str(event.location) if event.location else '' + } + formatted_events.append(formatted_event) + + # Sort by start time + formatted_events.sort(key=lambda e: e.get('start', '')) + + self.events = formatted_events + logger.info(f"[Calendar] Found {len(self.events)} events using Python API") + self.emit_events_changed(self.events) + + except Exception as e: + logger.error(f"[Calendar] Error using khal Python API: {e}") + # Fall back to subprocess method + self.update_events_subprocess() + + def update_events_subprocess(self): + """Fetch today's events using khal subprocess (fallback)""" # Get khal path from config khal_path = CALENDAR.get("khal_path", "khal") @@ -115,56 +163,38 @@ class CalendarService: except json.JSONDecodeError: continue - # Filter events for today - both past and upcoming - now = datetime.now() - current_time = now.strftime("%H:%M") - current_date = now.strftime("%m-%d") - - past_events = [] - upcoming_events = [] - - for event in all_events: - event_date = ( - event.get("start", "").split()[0] if event.get("start") else "" - ) - event_start_time = ( - event.get("start", "").split()[1] if event.get("start") else "" - ) - event_end_time = ( - event.get("end", "").split()[1] if event.get("end") else "" - ) - - # Only process events from today - if event_date == current_date: - if not event_end_time: # All-day events - upcoming_events.append(event) - elif event_end_time > current_time: # Haven't ended yet - upcoming_events.append(event) - elif event_end_time <= current_time: # Already ended - past_events.append(event) - - # Sort past events by start time (most recent first) - past_events.sort(key=lambda e: e.get("start", ""), reverse=True) - - # Take up to 3 most recent past events and up to 5 upcoming events - selected_past = past_events[:3] - selected_upcoming = upcoming_events[:5] - - # Combine: past events first, then upcoming events - self.events = selected_past + selected_upcoming - logger.info(f"[Calendar] Found {len(self.events)} upcoming events") - for i, event in enumerate(self.events): - logger.info( - f"[Calendar] Event {i+1}: {event.get('title', 'No title')} at {event.get('start', 'No time')}" - ) + self.events = all_events + logger.info(f"[Calendar] Found {len(self.events)} events using subprocess") + self.emit_events_changed(self.events) + else: + self.events = [] self.emit_events_changed(self.events) except subprocess.CalledProcessError as e: logger.error(f"[Calendar] Failed to fetch events: {e}") self.events = [] + self.emit_events_changed(self.events) except Exception as e: logger.error(f"[Calendar] Error processing events: {e}") self.events = [] + self.emit_events_changed(self.events) + + 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 + + # Try Python API first, fall back to subprocess + if KHAL_AVAILABLE: + logger.info("[Calendar] Using khal Python API") + self.update_events_python_api() + else: + logger.info("[Calendar] Using khal subprocess") + self.update_events_subprocess() class CalendarPopup(Window): diff --git a/flake.nix b/flake.nix index 1217095..98e3da5 100644 --- a/flake.nix +++ b/flake.nix @@ -100,6 +100,35 @@ default = { enable = false; }; description = "Stylix configuration passed from the stylix module"; }; + calendar = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to enable the calendar widget"; + }; + khal_path = lib.mkOption { + type = lib.types.str; + default = "khal"; + description = "Path to the khal binary"; + }; + }; + notmuch = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to enable the notmuch email widget"; + }; + notmuch_path = lib.mkOption { + type = lib.types.str; + default = "notmuch"; + description = "Path to the notmuch binary"; + }; + emacsclient_command = lib.mkOption { + type = lib.types.str; + default = "emacsclient"; + description = "Path to the emacsclient binary"; + }; + }; }; }; default = { @@ -108,6 +137,15 @@ height = 40; window_title.enable = true; stylix.enable = false; + calendar = { + enable = true; + khal_path = "khal"; + }; + notmuch = { + enable = true; + notmuch_path = "notmuch"; + emacsclient_command = "emacsclient"; + }; }; }; }; diff --git a/nix/derivation.nix b/nix/derivation.nix index f369c8b..f6f8ea1 100644 --- a/nix/derivation.nix +++ b/nix/derivation.nix @@ -12,6 +12,9 @@ wrapGAppsHook3, playerctl, webp-pixbuf-loader, + notmuch, + khal, + emacs, ... }: @@ -38,6 +41,8 @@ python3Packages.buildPythonApplication { gdk-pixbuf playerctl webp-pixbuf-loader + notmuch + khal ]; dependencies = with python3Packages; [ @@ -60,13 +65,19 @@ python3Packages.buildPythonApplication { cp scripts/launcher.py $out/bin/bar chmod +x $out/bin/bar + runHook postInstall ''; preFixup = '' makeWrapperArgs+=("''${gappsWrapperArgs[@]}") + makeWrapperArgs+=(--prefix PATH : ${lib.makeBinPath [ khal notmuch emacs ]}) ''; + passthru = { + inherit khal notmuch emacs; + }; + meta = { changelog = ""; description = ''