✨ feat(launcher/plugins/calendar): add visual calendar widget with navigation buttons and Enter key support
This commit is contained in:
3 files changed
+449
No files matched your search
@@ -8,6 +8,7 @@
|
||||
@import url("./styles/osd.css");
|
||||
@import url("./styles/launcher.css");
|
||||
@import url("./styles/kanban.css");
|
||||
@import url("./styles/calendar.css");
|
||||
|
||||
* {
|
||||
all: unset;
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
"""
|
||||
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}")
|
||||
@@ -0,0 +1,79 @@
|
||||
/* Main calendar widget container */
|
||||
#calendar-widget {
|
||||
border: 4px solid var(--surface);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
#calendar-header-box {
|
||||
border-radius: 16px;
|
||||
border: 2px solid var(--surface);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* Navigation buttons */
|
||||
#calendar-nav-button {
|
||||
background-color: var(--surface);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
#calendar-nav-button:hover {
|
||||
background-color: var(--surface-bright);
|
||||
}
|
||||
|
||||
#calendar-nav-button:active {
|
||||
background-color: var(--primary);
|
||||
|
||||
}
|
||||
|
||||
/* Month/Year header */
|
||||
#calendar-month-year {
|
||||
color: var(--foreground);
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
padding: 4px 0;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* Calendar grid */
|
||||
#calendar-grid {
|
||||
border-radius: 16px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* Day headers (Mon, Tue, Wed, etc.) */
|
||||
#calendar-day-header {
|
||||
color: var(--primary);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
|
||||
#calendar-day-label,
|
||||
#calendar-day-empty {
|
||||
padding: 2px;
|
||||
min-width: 23px;
|
||||
min-height: 23px;
|
||||
}
|
||||
|
||||
|
||||
#calendar-day-label.today {
|
||||
background-color: var(--foreground);
|
||||
color: var(--shadow);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
#calendar-day-label.weekend {
|
||||
color: var(--red-dim);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user