diff --git a/Makefile b/Makefile index a056669..9939e57 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,2 @@ run: - python -m bar.main --config ./example.yaml + python -m bar.main --config ./example-stylix-dev.yaml diff --git a/README.md b/README.md index 56f5c2b..da38289 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ # Todo + +# Ideas +## Org-mode integration - https://github.com/jlumpe/pyorg - https://github.com/jlumpe/ox-json +## Emails not seen +with notmuch +## notch power bar +- show the power around the notch +- show watts charging/discharging in bar +## Screenshot menu +## Media Playing diff --git a/bar/modules/bar.py b/bar/modules/bar.py index 8c95eb6..42cc0a8 100644 --- a/bar/modules/bar.py +++ b/bar/modules/bar.py @@ -8,6 +8,7 @@ from fabric.widgets.centerbox import CenterBox 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 fabric.widgets.wayland import WaylandWindow as Window from fabric.system_tray.widgets import SystemTray from fabric.river.widgets import ( @@ -52,7 +53,23 @@ class StatusBar(Window): buttons_factory=lambda ws_id: RiverWorkspaceButton(id=ws_id, label=None), river_service=self.river, ) - self.date_time = DateTime(name="date-time", formatters="%d %b - %H:%M") + # Create calendar components (refresh every 2 minutes) + self.calendar_service = CalendarService(update_interval=120000) + self.calendar_popup = CalendarPopup() + self.calendar_popup_visible = False + + # Create clickable datetime widget + from fabric.widgets.button import Button + datetime_widget = DateTime(name="date-time", formatters="%d %b - %H:%M") + self.date_time = Button( + name="date-time-button", + child=datetime_widget, + on_clicked=self.toggle_calendar, + style="background: transparent; border: none; padding: 0; margin: 0; box-shadow: none;" + ) + + # Connect calendar service to popup + self.calendar_service.connect("events-changed", self.update_calendar_display) self.system_tray = tray self.active_window = RiverActiveWindow( @@ -83,6 +100,7 @@ class StatusBar(Window): self.battery = None if BATTERY["enable"]: self.battery = Battery() + self.status_container = Box( name="widgets-container", spacing=4, @@ -142,7 +160,35 @@ class StatusBar(Window): self.show_all() + def __del__(self): + """Cleanup when bar is destroyed""" + if hasattr(self, 'calendar_service'): + self.calendar_service.stop_monitoring() + def update_progress_bars(self, service, cpu_percent, memory_percent): """Update progress bars when system stats change""" self.cpu_progress_bar.value = cpu_percent self.ram_progress_bar.value = memory_percent + + def toggle_calendar(self, button=None): + """Toggle the calendar popup when datetime is clicked""" + from loguru import logger + logger.info(f"[Calendar] DateTime clicked, popup_visible: {self.calendar_popup_visible}") + + if self.calendar_popup_visible: + logger.info("[Calendar] Hiding calendar popup") + self.calendar_popup.set_visible(False) + self.calendar_popup_visible = False + else: + logger.info("[Calendar] Showing calendar popup") + # Use cached events - no need to refresh on click + cached_events = self.calendar_service.get_cached_events() + logger.info(f"[Calendar] Using {len(cached_events)} cached events") + self.calendar_popup.update_events_display(cached_events) + self.calendar_popup.set_visible(True) + self.calendar_popup.show_all() + self.calendar_popup_visible = True + + def update_calendar_display(self, service, events): + """Update the calendar popup with events""" + self.calendar_popup.update_events_display(events) diff --git a/bar/modules/calendar.py b/bar/modules/calendar.py new file mode 100644 index 0000000..8ca5464 --- /dev/null +++ b/bar/modules/calendar.py @@ -0,0 +1,282 @@ +import json +import subprocess +from datetime import datetime +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 fabric.widgets.wayland import WaylandWindow as Window +from loguru import logger + + +class CalendarService: + def __init__(self, update_interval=300000): # 5 minutes default + self.events = [] + self.callbacks = [] + self._update_interval = update_interval + self._timer_id = None + + # Initial load + self.update_events() + # Start periodic updates + self.start_monitoring() + + def connect(self, signal_name, callback): + """Simple callback system to replace signals""" + if signal_name == "events-changed": + self.callbacks.append(callback) + + def emit_events_changed(self, events): + """Emit events changed to all callbacks""" + for callback in self.callbacks: + callback(self, events) + + def start_monitoring(self): + """Start periodic event 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"[Calendar] Started periodic updates every {self._update_interval/1000/60:.1f} minutes") + + def stop_monitoring(self): + """Stop periodic event 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("[Calendar] Stopped periodic updates") + + def _periodic_update(self): + """Periodic update callback""" + logger.info("[Calendar] Performing periodic events update") + self.update_events() + return True # Keep the timer running + + def get_cached_events(self): + """Get cached events without triggering update""" + return self.events + + def update_events(self): + """Fetch today's events from khal""" + try: + result = subprocess.run( + ["khal", "list", "--json", "title", "--json", "start", "--json", "end", "--json", "location", "today"], + capture_output=True, + text=True, + check=True + ) + + if result.stdout.strip(): + lines = result.stdout.strip().split('\n') + all_events = [] + + for line in lines: + if line.strip(): + try: + events = json.loads(line) + all_events.extend(events) + 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.emit_events_changed(self.events) + + except subprocess.CalledProcessError as e: + logger.error(f"[Calendar] Failed to fetch events: {e}") + self.events = [] + except Exception as e: + logger.error(f"[Calendar] Error processing events: {e}") + self.events = [] + + +class CalendarPopup(Window): + def __init__(self, **kwargs): + super().__init__( + name="calendar-popup", + layer="top", + anchor="top right", + margin="40px 10px 0px 0px", + exclusivity="none", + visible=False, + all_visible=False, + **kwargs + ) + + # Events container + self.events_box = Box( + name="events-box", + orientation="v", + spacing=4, + style="min-width: 300px; min-height: 100px;" + ) + + # Add a test label to make sure popup is working + test_label = Label( + "Calendar Events", + name="calendar-title" + ) + + container = Box( + orientation="v", + spacing=4, + children=[test_label, self.events_box] + ) + + self.children = container + + # Set explicit size + self.set_size_request(320, 200) + + def update_events_display(self, events): + """Update the events display""" + logger.info(f"[Calendar] Updating popup with {len(events)} events") + + # Clear existing children first + self.events_box.children = [] + + if not events: + logger.info("[Calendar] No events, showing 'no events' message") + no_events_label = Label( + "No upcoming events today", + name="no-events" + ) + self.events_box.add(no_events_label) + return + + # Check current time for determining past vs upcoming + now = datetime.now() + current_time = now.strftime("%H:%M") + + for i, event in enumerate(events): + logger.info(f"[Calendar] Processing event {i+1} for display") + title = event.get("title", "No title") + start_time = event.get("start", "").split()[1] if event.get("start") else "" + end_time = event.get("end", "").split()[1] if event.get("end") else "" + location = event.get("location", "") + + # Determine if event is in the past + is_past = end_time and end_time <= current_time + + # Format time display + time_str = "" + if start_time and end_time: + time_str = f"{start_time} - {end_time}" + elif start_time: + time_str = start_time + + logger.info(f"[Calendar] Creating widget for: {title} ({time_str}) - {'Past' if is_past else 'Upcoming'}") + + # Create event item with CSS classes for theming + event_status = "past" if is_past else "upcoming" + event_box = Box( + name="event-item", + orientation="v", + spacing=2, + style_classes=[f"event-item", event_status] + ) + + # Title with status prefix + title_prefix = "✓ " if is_past else "" + title_label = Label( + f"{title_prefix}{title}", + name="event-title", + style_classes=["event-title", event_status] + ) + event_box.add(title_label) + + if time_str: + time_label = Label( + time_str, + name="event-time", + style_classes=["event-time", event_status] + ) + event_box.add(time_label) + + if location: + location_label = Label( + f"📍 {location}", + name="event-location", + style_classes=["event-location", event_status] + ) + event_box.add(location_label) + + self.events_box.add(event_box) + logger.info(f"[Calendar] Added event widget to events_box") + + # Force refresh the popup display + self.events_box.show_all() + logger.info(f"[Calendar] Finished updating popup") + + +class CalendarWidget(Button): + def __init__(self, **kwargs): + super().__init__( + name="calendar-widget", + child=Image(icon_name="x-office-calendar-symbolic", icon_size=16), + on_clicked=self.toggle_events, + **kwargs + ) + + self.service = CalendarService() + self.service.connect("events-changed", self.update_events_display) + + # Create popup window + self.popup = CalendarPopup() + self.popup_visible = False + logger.info("[Calendar] Calendar widget initialized with popup") + + # Initial update + self.update_events_display(self.service, self.service.events) + + def toggle_events(self, button=None): + """Toggle the visibility of the events popup""" + logger.info(f"[Calendar] Button clicked, popup_visible: {self.popup_visible}") + + if self.popup_visible: + logger.info("[Calendar] Hiding popup") + self.popup.set_visible(False) + self.popup_visible = False + else: + logger.info("[Calendar] Showing popup") + # Refresh events when opening + self.service.update_events() + self.popup.set_visible(True) + self.popup.show_all() + self.popup_visible = True + + def update_events_display(self, service, events): + """Update the events display in popup""" + self.popup.update_events_display(events) \ No newline at end of file diff --git a/bar/modules/stylix.py b/bar/modules/stylix.py index 6a01b9c..35638a9 100644 --- a/bar/modules/stylix.py +++ b/bar/modules/stylix.py @@ -125,6 +125,92 @@ def generate_stylix_css(): border-radius: 12px; }} +#date-time-button {{ + background: transparent; + border: none; + padding: 0; + margin: 0; + box-shadow: none; +}} + +/* Calendar popup */ +#calendar-popup {{ + background-color: #{colors["base00"]}; + border: solid 2px #{colors["base02"]}; + border-radius: 8px; +}} + +#calendar-title {{ + color: #{colors["base05"]}; + font-weight: bold; + margin-bottom: 8px; +}} + +#events-box {{ + background-color: #{colors["base00"]}; + border: solid 1px #{colors["base02"]}; + border-radius: 8px; + padding: 12px; +}} + +#no-events {{ + color: #{colors["base03"]}; +}} + +/* Calendar event items */ +.event-item {{ + border-radius: 4px; + padding: 6px; + margin: 2px 0px; +}} + +.event-item.upcoming {{ + background-color: #{colors["base01"]}; +}} + +.event-item.past {{ + background-color: #{colors["base01"]}; + opacity: 0.6; +}} + +.event-title {{ + font-weight: bold; + font-size: 12px; +}} + +.event-title.upcoming {{ + color: #{colors["base05"]}; +}} + +.event-title.past {{ + color: #{colors["base04"]}; +}} + +.event-time {{ + font-size: 11px; +}} + +.event-time.upcoming {{ + color: #{colors["base04"]}; +}} + +.event-time.past {{ + color: #{colors["base03"]}; +}} + +.event-location {{ + font-size: 11px; +}} + +.event-location.upcoming {{ + color: #{colors["base03"]}; +}} + +.event-location.past {{ + color: #{colors["base03"]}; + opacity: 0.8; +}} + /* Tooltips */ tooltip {{ border: solid 2px; diff --git a/bar/styles/calendar.css b/bar/styles/calendar.css new file mode 100644 index 0000000..182c14c --- /dev/null +++ b/bar/styles/calendar.css @@ -0,0 +1,90 @@ +/* Calendar widget styling (fallback when Stylix is disabled) */ + +/* Date time button */ +#date-time-button { + background: transparent; + border: none; + padding: 0; + margin: 0; + box-shadow: none; +} + +/* Calendar popup */ +#calendar-popup { + background-color: var(--background-alt); + border: solid 2px var(--surface); + border-radius: 8px; +} + +#calendar-title { + color: var(--foreground); + font-weight: bold; + margin-bottom: 8px; +} + +#events-box { + background-color: var(--background-alt); + border: solid 1px var(--surface); + border-radius: 8px; + padding: 12px; +} + +#no-events { + color: var(--muted); + font-size: 12px; + padding: 4px; +} + +/* Calendar event items */ +.event-item { + border-radius: 4px; + padding: 6px; + margin: 2px 0px; +} + +.event-item.upcoming { + background-color: var(--surface); +} + +.event-item.past { + background-color: var(--surface); + opacity: 0.6; +} + +.event-title { + font-weight: bold; + font-size: 12px; +} + +.event-title.upcoming { + color: var(--foreground); +} + +.event-title.past { + color: var(--muted); +} + +.event-time { + font-size: 11px; +} + +.event-time.upcoming { + color: var(--muted); +} + +.event-time.past { + color: var(--muted-dark); +} + +.event-location { + font-size: 11px; +} + +.event-location.upcoming { + color: var(--muted-dark); +} + +.event-location.past { + color: var(--muted-dark); + opacity: 0.8; +} \ No newline at end of file diff --git a/bar/styles/main.css b/bar/styles/main.css index e496b4d..e3dc704 100644 --- a/bar/styles/main.css +++ b/bar/styles/main.css @@ -4,6 +4,7 @@ @import url("./vinyl.css"); @import url("./bar.css"); @import url("./finder.css"); +@import url("./calendar.css"); /* unset so we can style everything from the ground up. */ diff --git a/example-stylix-dev.yaml b/example-stylix-dev.yaml index 6f9ad05..e550c64 100644 --- a/example-stylix-dev.yaml +++ b/example-stylix-dev.yaml @@ -2,7 +2,7 @@ height: 42 window_title: enable: false vinyl: - enable: true + enable: false battery: enable: true stylix: