Files
Modus/modules/launcher/plugins/calendar.py
T
2025-06-26 16:10:17 +05:45

405 lines
14 KiB
Python

import calendar
import datetime
from typing import List
import gi
from fabric.widgets.label import Label
from gi.repository import Gtk
import utils.icons as icons
from modules.launcher.plugin_base import PluginBase
from modules.launcher.result import Result
gi.require_version("Gtk", "3.0")
class CalendarWidget(Gtk.Box):
"""Visual calendar widget showing month grid with dates and navigation buttons."""
def __init__(self, month: int, year: int, plugin_instance=None):
super().__init__(
name="calendar-widget", orientation=Gtk.Orientation.VERTICAL, spacing=2
)
# Set size constraints to fit within launcher (following CSS)
self.set_size_request(540, 240)
self.set_vexpand(False)
self.set_hexpand(True)
self.display_month = month
self.display_year = year
self.today = datetime.date.today()
self.plugin = plugin_instance
# Make widget focusable for keyboard events
self.set_can_focus(True)
self.connect("key-press-event", self._on_key_press)
self.setup_ui()
def setup_ui(self):
"""Setup the calendar UI with navigation buttons."""
# Header with navigation buttons
header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
header_box.set_halign(Gtk.Align.CENTER)
header_box.set_name("calendar-header-box")
# Previous button
prev_button = Gtk.Button(
child=Label(markup=icons.chevron_left, name="calendar-nav-button")
)
prev_button.connect("clicked", self._on_previous_clicked)
# Month/Year label (following CSS naming)
month_name = calendar.month_name[self.display_month]
self.month_label = Label(
name="calendar-month-year",
label=f"{month_name} {self.display_year}",
h_align="center",
)
# Fixed width for consistent layout
self.month_label.set_size_request(150, -1)
# Next button
next_button = Gtk.Button(
child=Label(markup=icons.chevron_right, name="calendar-nav-button")
)
next_button.connect("clicked", self._on_next_clicked)
# Add to header box
header_box.pack_start(prev_button, False, False, 0)
header_box.pack_start(self.month_label, True, True, 0)
header_box.pack_start(next_button, False, False, 0)
# Calendar grid (following CSS naming)
grid = Gtk.Grid(
name="calendar-grid",
column_spacing=1,
row_spacing=1,
column_homogeneous=True,
row_homogeneous=True,
)
# Day headers (following CSS naming)
day_names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
for i, day_name in enumerate(day_names):
day_header = Label(
name="calendar-day-header", label=day_name, h_align="center"
)
grid.attach(day_header, i, 0, 1, 1)
# Calendar days (following CSS naming)
cal = calendar.monthcalendar(self.display_year, self.display_month)
for week_num, week in enumerate(cal, start=1):
for day_num, day in enumerate(week):
if day == 0:
# Empty cell (following CSS naming)
empty = Label(name="calendar-day-empty", label="")
grid.attach(empty, day_num, week_num, 1, 1)
else:
# Day label (following CSS naming)
day_label = Label(
name="calendar-day-label", label=str(day), h_align="center"
)
# Highlight today (CSS class)
if (
day == self.today.day
and self.display_month == self.today.month
and self.display_year == self.today.year
):
day_label.get_style_context().add_class("today")
# Highlight weekends (CSS class)
if day_num in [6]: # Saturday
day_label.get_style_context().add_class("weekend")
grid.attach(day_label, day_num, week_num, 1, 1)
# Add to main box
self.pack_start(header_box, False, False, 0)
self.pack_start(grid, True, True, 0)
self.show_all()
def _on_previous_clicked(self, button):
"""Handle previous month button click."""
if self.plugin:
self.plugin._navigate_to_previous()
def _on_next_clicked(self, button):
"""Handle next month button click."""
if self.plugin:
self.plugin._navigate_to_next()
def _on_key_press(self, widget, event):
"""Handle keyboard navigation for calendar."""
from gi.repository import Gdk
keyval = event.keyval
# Left arrow - previous month
if keyval == Gdk.KEY_Left:
if self.plugin:
self.plugin._navigate_to_previous()
return True
# Right arrow - next month
if keyval == Gdk.KEY_Right:
if self.plugin:
self.plugin._navigate_to_next()
return True
# Home - go to current month
if keyval == Gdk.KEY_Home:
if self.plugin:
self.plugin._reset_to_current_month()
self.plugin._force_launcher_refresh()
return True
return False
def update_display(self, month: int, year: int):
"""Update the calendar to show a different month/year."""
self.display_month = month
self.display_year = year
# Update month label
month_name = calendar.month_name[month]
self.month_label.set_label(f"{month_name} {year}")
# Rebuild the calendar grid
# Find and remove the old grid
for child in self.get_children():
if isinstance(child, Gtk.Grid):
self.remove(child)
child.destroy()
break
# Create new grid (following CSS naming)
grid = Gtk.Grid(
name="calendar-grid",
column_spacing=1,
row_spacing=1,
column_homogeneous=True,
row_homogeneous=True,
)
# Day headers (following CSS naming)
day_names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
for i, day_name in enumerate(day_names):
day_header = Label(
name="calendar-day-header", label=day_name, h_align="center"
)
grid.attach(day_header, i, 0, 1, 1)
# Calendar days (following CSS naming)
cal = calendar.monthcalendar(year, month)
for week_num, week in enumerate(cal, start=1):
for day_num, day in enumerate(week):
if day == 0:
# Empty cell (following CSS naming)
empty = Label(name="calendar-day-empty", label="")
grid.attach(empty, day_num, week_num, 1, 1)
else:
# Day label (following CSS naming)
day_label = Label(
name="calendar-day-label", label=str(day), h_align="center"
)
# Highlight today (CSS class)
if (
day == self.today.day
and month == self.today.month
and year == self.today.year
):
day_label.get_style_context().add_class("today")
# Highlight weekends (CSS class)
if day_num in [6]: # Saturday and Sunday
day_label.get_style_context().add_class("weekend")
grid.attach(day_label, day_num, week_num, 1, 1)
# Add new grid
self.pack_start(grid, True, True, 0)
grid.show_all()
class CalendarPlugin(PluginBase):
"""
Calendar plugin for the launcher.
Simple text-based calendar with working Enter key navigation.
"""
def __init__(self):
super().__init__()
self.display_name = "Calendar"
self.description = "Calendar with visual dates and navigation buttons"
self._current_widget = None
# Current display state
self._current_month = None
self._current_year = None
def initialize(self):
"""Initialize the Calendar plugin."""
self.set_triggers(["cal"])
self.description = "Calendar with visual dates and navigation. Use ← → arrows for months, Home for current month"
def cleanup(self):
"""Cleanup the Calendar plugin."""
if self._current_widget:
if self._current_widget.get_parent():
self._current_widget.get_parent().remove(self._current_widget)
self._current_widget.destroy()
self._current_widget = None
# Reset to current month when cleaning up
self._reset_to_current_month()
def query(self, query_string: str) -> List[Result]:
"""Process Calendar queries."""
results = []
query = query_string.strip().lower()
today = datetime.date.today()
# Handle date queries
if query in ["today", "now"]:
date_str = today.strftime("%A, %B %d, %Y")
results.append(
Result(
title=date_str,
subtitle="Today's date • Click to copy",
icon_markup=icons.calendar,
action=lambda: self._copy_to_clipboard(date_str),
relevance=1.0,
plugin_name=self.display_name,
)
)
return results
elif query == "tomorrow":
tomorrow = today + datetime.timedelta(days=1)
date_str = tomorrow.strftime("%A, %B %d, %Y")
results.append(
Result(
title=date_str,
subtitle="Tomorrow's date • Click to copy",
icon_markup=icons.calendar,
action=lambda: self._copy_to_clipboard(date_str),
relevance=1.0,
plugin_name=self.display_name,
)
)
return results
elif query == "yesterday":
yesterday = today - datetime.timedelta(days=1)
date_str = yesterday.strftime("%A, %B %d, %Y")
results.append(
Result(
title=date_str,
subtitle="Yesterday's date • Click to copy",
icon_markup=icons.calendar,
action=lambda: self._copy_to_clipboard(date_str),
relevance=1.0,
plugin_name=self.display_name,
)
)
return results
# Clean up previous widget
self.cleanup()
# Check if we should reset to current month
# This happens when the query is empty or just the trigger
if not query or query in ["", "cal"]:
self._reset_to_current_month()
# Determine which month to show
if self._current_month is None:
# First time - show current month
display_month = today.month
display_year = today.year
else:
# Use stored month/year
display_month = self._current_month
display_year = self._current_year
# Update stored state
self._current_month = display_month
self._current_year = display_year
# Create calendar widget with navigation buttons
calendar_widget = CalendarWidget(display_month, display_year, self)
self._current_widget = calendar_widget
month_name = calendar.month_name[display_month]
results.append(
Result(
title=f"Calendar - {month_name} {display_year}",
subtitle="← → navigate months, Home for current month",
icon_markup=icons.calendar,
action=lambda: None,
relevance=1.0,
plugin_name=self.display_name,
custom_widget=calendar_widget,
data={"type": "calendar_display"},
)
)
return results
def _navigate_to_next(self):
"""Navigate to next month (called by button click)."""
if self._current_month and self._current_year:
next_month, next_year = self._advance_month(
self._current_month, self._current_year, 1
)
self._current_month = next_month
self._current_year = next_year
# Update the calendar widget display
if self._current_widget:
self._current_widget.update_display(next_month, next_year)
def _navigate_to_previous(self):
"""Navigate to previous month (called by button click)."""
if self._current_month and self._current_year:
prev_month, prev_year = self._advance_month(
self._current_month, self._current_year, -1
)
self._current_month = prev_month
self._current_year = prev_year
# Update the calendar widget display
if self._current_widget:
self._current_widget.update_display(prev_month, prev_year)
def _advance_month(self, month: int, year: int, direction: int) -> tuple[int, int]:
"""Advance month by direction (1 for next, -1 for previous)."""
if direction > 0: # Next
if month == 12:
return 1, year + 1
else:
return month + 1, year
else: # Previous
if month == 1:
return 12, year - 1
else:
return month - 1, year
def _reset_to_current_month(self):
"""Reset calendar to current month."""
today = datetime.date.today()
self._current_month = today.month
self._current_year = today.year
def _copy_to_clipboard(self, text: str):
"""Copy text to clipboard."""
try:
import subprocess
subprocess.run(["wl-copy"], input=text.encode(), check=True)
print(f"Copied to clipboard: {text}")
except subprocess.CalledProcessError:
print(f"Failed to copy to clipboard: {text}")