204 files changed
+51505
No files matched your search
@@ -0,0 +1,448 @@
|
||||
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
|
||||
Reference in new issue
Block a user