import re import subprocess import threading from datetime import datetime, timedelta from typing import Dict, List, Optional from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result class Reminder: """ Represents a single reminder with its timer and metadata. """ def __init__( self, reminder_id: int, message: str, target_time: datetime, timer: threading.Timer, ): self.id = reminder_id self.message = message self.target_time = target_time self.timer = timer self.created_time = datetime.now() def cancel(self): """Cancel this reminder.""" if self.timer: self.timer.cancel() def get_time_remaining(self) -> str: """Get formatted time remaining until reminder.""" now = datetime.now() if self.target_time <= now: return "Overdue" delta = self.target_time - now total_seconds = int(delta.total_seconds()) if total_seconds < 60: return f"{total_seconds}s" elif total_seconds < 3600: minutes = total_seconds // 60 seconds = total_seconds % 60 return f"{minutes}m {seconds}s" if seconds > 0 else f"{minutes}m" else: hours = total_seconds // 3600 minutes = (total_seconds % 3600) // 60 return f"{hours}h {minutes}m" if minutes > 0 else f"{hours}h" def get_target_time_str(self) -> str: """Get formatted target time.""" return self.target_time.strftime("%H:%M") class RemindersPlugin(PluginBase): """ Time-based reminders plugin for the launcher. """ def __init__(self): super().__init__() self.display_name = "Reminders" self.description = "Set time-based reminders with notifications" self.reminders: Dict[int, Reminder] = {} self.next_id = 1 # Regex patterns for time parsing self.time_patterns = { "relative_time": re.compile(r"^(\d+)([smhd])$"), # 5m, 30s, 2h, 1d "absolute_time": re.compile(r"^(\d{1,2}):(\d{2})$"), # 14:30, 9:15 "relative_with_unit": re.compile( r"^(\d+)\s*(min|mins|minute|minutes|hour|hours|sec|seconds|day|days)$", re.IGNORECASE, ), } def initialize(self): """Initialize the reminders plugin.""" self.set_triggers(["remind"]) def cleanup(self): """Cleanup the reminders plugin.""" # Cancel all active reminders for reminder in self.reminders.values(): reminder.cancel() self.reminders.clear() def _send_notification(self, title: str, message: str): """Send a desktop notification using notify-send.""" try: subprocess.run( ["notify-send", "-a", "Reminders", "-i", "alarm-clock", title, message], check=False, ) except Exception as e: print(f"Failed to send notification: {e}") def _parse_time_input(self, time_str: str) -> Optional[datetime]: """ Parse various time input formats and return target datetime. Supported formats: - 5m, 30s, 2h, 1d (relative time) - 14:30, 9:15 (absolute time today) - 5 minutes, 2 hours (relative with full unit names) """ time_str = time_str.strip().lower() # Try relative time format (5m, 30s, 2h, 1d) match = self.time_patterns["relative_time"].match(time_str) if match: value, unit = match.groups() value = int(value) if unit == "s": delta = timedelta(seconds=value) elif unit == "m": delta = timedelta(minutes=value) elif unit == "h": delta = timedelta(hours=value) elif unit == "d": delta = timedelta(days=value) else: return None return datetime.now() + delta # Try absolute time format (14:30, 9:15) match = self.time_patterns["absolute_time"].match(time_str) if match: hour, minute = map(int, match.groups()) if 0 <= hour <= 23 and 0 <= minute <= 59: target = datetime.now().replace( hour=hour, minute=minute, second=0, microsecond=0 ) # If the time has already passed today, schedule for tomorrow if target <= datetime.now(): target += timedelta(days=1) return target # Try relative time with full unit names match = self.time_patterns["relative_with_unit"].match(time_str) if match: value, unit = match.groups() value = int(value) unit = unit.lower() if unit in ["sec", "seconds"]: delta = timedelta(seconds=value) elif unit in ["min", "mins", "minute", "minutes"]: delta = timedelta(minutes=value) elif unit in ["hour", "hours"]: delta = timedelta(hours=value) elif unit in ["day", "days"]: delta = timedelta(days=value) else: return None return datetime.now() + delta return None def _create_reminder(self, time_str: str, message: str) -> Optional[Reminder]: """Create a new reminder with the given time and message.""" target_time = self._parse_time_input(time_str) if not target_time: return None # Calculate delay in seconds delay = (target_time - datetime.now()).total_seconds() if delay <= 0: return None # Create timer that will trigger the notification timer = threading.Timer(delay, self._trigger_reminder, [self.next_id, message]) # Create reminder object reminder = Reminder(self.next_id, message, target_time, timer) # Store reminder and start timer self.reminders[self.next_id] = reminder timer.start() # Increment ID for next reminder self.next_id += 1 return reminder def _trigger_reminder(self, reminder_id: int, message: str): """Trigger a reminder notification and remove it from active reminders.""" # Send notification self._send_notification("⏰ Reminder", message) # Remove from active reminders if reminder_id in self.reminders: del self.reminders[reminder_id] def _cancel_reminder(self, reminder_id: Optional[int] = None) -> int: """Cancel a specific reminder or all reminders. Returns number of cancelled reminders.""" if reminder_id is not None: if reminder_id in self.reminders: self.reminders[reminder_id].cancel() del self.reminders[reminder_id] return 1 return 0 else: # Cancel all reminders count = len(self.reminders) for reminder in self.reminders.values(): reminder.cancel() self.reminders.clear() return count def _format_time_remaining(self, total_seconds: float) -> str: """Format time remaining in a human-readable way.""" total_seconds = int(total_seconds) if total_seconds < 60: return f"{total_seconds}s" elif total_seconds < 3600: minutes = total_seconds // 60 seconds = total_seconds % 60 return f"{minutes}m {seconds}s" if seconds > 0 else f"{minutes}m" else: hours = total_seconds // 3600 minutes = (total_seconds % 3600) // 60 return f"{hours}h {minutes}m" if minutes > 0 else f"{hours}h" def _create_and_confirm_reminder(self, time_str: str, message: str): """Actually create the reminder when the user presses Enter.""" reminder = self._create_reminder(time_str, message) if reminder: time_remaining = reminder.get_time_remaining() self._send_notification( "✅ Reminder Created", f"Reminder set for {time_remaining}: {message}" ) else: self._send_notification( "❌ Failed to Create Reminder", "The specified time may be in the past" ) def query(self, query_string: str) -> List[Result]: """Process reminder queries.""" results = [] query = query_string.strip() if not query: # Show help and active reminders count active_count = len(self.reminders) results.append( Result( title="Reminders Help", subtitle=f"Active reminders: { active_count } | Usage: remind 5m Take a break", icon_name="alarm-timer", action=lambda: None, relevance=1.0, plugin_name=self.display_name, data={"type": "help"}, ) ) # Show quick examples examples = [ ("remind 5m Take a break", "Set 5 minute reminder"), ("remind 14:30 Meeting", "Set reminder for 2:30 PM"), ("remind list", "List active reminders"), ("remind cancel", "Cancel all reminders"), ] for example, desc in examples: results.append( Result( title=example, subtitle=desc, icon_name="alarm-timer", action=lambda: None, relevance=0.8, plugin_name=self.display_name, data={"type": "example"}, ) ) return results # Handle list command if query.lower() in ["list", "ls", "show"]: if not self.reminders: results.append( Result( title="No Active Reminders", subtitle="Use 'remind 5m message' to set a reminder", icon_name="timer-off", action=lambda: None, relevance=1.0, plugin_name=self.display_name, data={"type": "empty_list"}, ) ) else: for reminder in sorted( self.reminders.values(), key=lambda r: r.target_time ): time_remaining = reminder.get_time_remaining() target_time = reminder.get_target_time_str() results.append( Result( title=f"#{reminder.id}: {reminder.message}", subtitle=f"In {time_remaining} (at {target_time})", icon_name="alarm-timer", action=lambda rid=reminder.id: self._cancel_reminder(rid), relevance=1.0, plugin_name=self.display_name, data={"type": "active_reminder", "id": reminder.id}, ) ) return results # Handle cancel command if query.lower().startswith("cancel") or query.lower().startswith("stop"): parts = query.split() if len(parts) == 1: # Cancel all reminders count = self._cancel_reminder() results.append( Result( title=f"Cancelled {count} Reminders", subtitle="All active reminders have been cancelled", icon_name="timer-off", action=lambda: None, relevance=1.0, plugin_name=self.display_name, data={"type": "cancel_all"}, ) ) else: # Try to cancel specific reminder by ID try: reminder_id = int(parts[1]) count = self._cancel_reminder(reminder_id) if count > 0: results.append( Result( title=f"Cancelled Reminder #{reminder_id}", subtitle="Reminder has been cancelled", icon_name="timer-off", action=lambda: None, relevance=1.0, plugin_name=self.display_name, data={"type": "cancel_specific"}, ) ) else: results.append( Result( title="Reminder Not Found", subtitle=f"No reminder with ID #{reminder_id}", icon_name="alert", action=lambda: None, relevance=0.5, plugin_name=self.display_name, data={"type": "error"}, ) ) except ValueError: results.append( Result( title="Invalid Reminder ID", subtitle="Please provide a valid reminder ID number", icon_name="alert", action=lambda: None, relevance=0.5, plugin_name=self.display_name, data={"type": "error"}, ) ) return results # Handle setting new reminders parts = query.split(None, 1) if len(parts) >= 1: time_str = parts[0] message = parts[1] if len(parts) > 1 else "Reminder" # Try to parse the time (but don't create the reminder yet!) target_time = self._parse_time_input(time_str) if target_time: # Calculate delay and check if it's valid delay = (target_time - datetime.now()).total_seconds() if delay > 0: # Show what would happen, but don't create the reminder yet time_remaining = self._format_time_remaining(delay) target_time_str = target_time.strftime("%H:%M") results.append( Result( title=f"Set Reminder: {message}", subtitle=f"Will remind in {time_remaining} (at { target_time_str })", icon_name="alarm-timer", action=lambda ts=time_str, msg=message: self._create_and_confirm_reminder( ts, msg ), relevance=1.0, plugin_name=self.display_name, data={ "type": "preview", "time_str": time_str, "message": message, }, ) ) else: results.append( Result( title="Time is in the Past", subtitle="Please specify a future time", icon_name="alert", action=lambda: None, relevance=0.5, plugin_name=self.display_name, data={"type": "error"}, ) ) else: # Invalid time format results.append( Result( title="Invalid Time Format", subtitle="Use formats like: 5m, 30s, 2h, 14:30, or '5 minutes'", icon_name="alert", action=lambda: None, relevance=0.5, plugin_name=self.display_name, data={"type": "error"}, ) ) return results