Files
Modus/modules/launcher/plugins/calendar.py
T

370 lines
13 KiB
Python

"""
Calendar plugin for the launcher.
Calendar with visual dates and working Enter key navigation.
"""
import calendar
import datetime
from typing import List
import gi
import utils.icons as icons
from fabric.widgets.label import Label
from gi.repository import Gtk
from fabric.widgets.button import Button
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
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"
)
self.month_label.set_size_request(150, -1) # Fixed width for consistent layout
# 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 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", "cal "])
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="Use ◀ ▶ buttons to navigate months",
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}")