334 lines
12 KiB
Python
334 lines
12 KiB
Python
"""
|
|
Pomodoro plugin for the launcher.
|
|
Implements the Pomodoro Technique timer with work/break cycles.
|
|
"""
|
|
|
|
import threading
|
|
import time
|
|
import subprocess
|
|
from typing import List
|
|
from modules.launcher.plugin_base import PluginBase
|
|
from modules.launcher.result import Result
|
|
import utils.icons as icons
|
|
|
|
|
|
class PomodoroTimer:
|
|
"""
|
|
Core Pomodoro timer implementation.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.is_break = True
|
|
self.timer = None
|
|
self.countdown_timer = None
|
|
self.remaining_till_long_break = 0
|
|
self.end_time = 0
|
|
self.pomodoro_duration = 0
|
|
self.break_duration = 0
|
|
self.long_break_duration = 0
|
|
self.count = 0
|
|
self.current_cycle = 0
|
|
self.total_cycles_completed = 0
|
|
|
|
def _send_notification(self, title, message, icon="🍅"):
|
|
"""Send a desktop notification."""
|
|
try:
|
|
subprocess.run(
|
|
["notify-send", "-a", "Pomodoro Timer", "-i", "timer", title, message],
|
|
check=False,
|
|
)
|
|
except Exception as e:
|
|
print(f"Failed to send notification: {e}")
|
|
|
|
def _send_countdown_notification(self, seconds_left):
|
|
"""Send countdown notification."""
|
|
try:
|
|
subprocess.run(
|
|
[
|
|
"notify-send",
|
|
"-a",
|
|
"Pomodoro Timer",
|
|
"-i",
|
|
"timer",
|
|
"-t",
|
|
"1000", # Show for 1 second
|
|
"⏰ Pomodoro Timer",
|
|
f"Time's up in {seconds_left} seconds!",
|
|
],
|
|
check=False,
|
|
)
|
|
except Exception as e:
|
|
print(f"Failed to send countdown notification: {e}")
|
|
|
|
def _schedule_countdown_notifications(self, duration_seconds):
|
|
"""Schedule countdown notifications for the last 3 seconds."""
|
|
if duration_seconds > 3:
|
|
# Schedule countdown notifications for 3, 2, 1 seconds before end
|
|
for countdown in [3, 2, 1]:
|
|
delay = duration_seconds - countdown
|
|
if delay > 0:
|
|
timer = threading.Timer(
|
|
delay, self._send_countdown_notification, [countdown]
|
|
)
|
|
timer.start()
|
|
|
|
def timeout(self):
|
|
"""Handle timer timeout and switch between work/break periods."""
|
|
if self.is_break:
|
|
# Starting work period
|
|
duration = self.pomodoro_duration * 60
|
|
self.timer = threading.Timer(duration, self.timeout)
|
|
self.end_time = time.time() + duration
|
|
self.current_cycle += 1
|
|
|
|
# Send notification for work period start
|
|
self._send_notification(
|
|
"🍅 Work Time!",
|
|
f"Starting work period {self.current_cycle} ({
|
|
self.pomodoro_duration
|
|
} minutes)",
|
|
)
|
|
|
|
# Schedule countdown notifications
|
|
self._schedule_countdown_notifications(duration)
|
|
|
|
self.timer.start()
|
|
else:
|
|
# Starting break period
|
|
self.remaining_till_long_break -= 1
|
|
self.total_cycles_completed += 1
|
|
|
|
if self.remaining_till_long_break == 0:
|
|
self.remaining_till_long_break = self.count
|
|
duration = self.long_break_duration * 60
|
|
|
|
# Send notification for long break
|
|
self._send_notification(
|
|
"🌟 Long Break Time!",
|
|
f"Take a long break ({
|
|
self.long_break_duration
|
|
} minutes)\nYou've completed {self.count} cycles! Total: {
|
|
self.total_cycles_completed
|
|
}",
|
|
)
|
|
|
|
else:
|
|
duration = self.break_duration * 60
|
|
|
|
# Send notification for short break
|
|
self._send_notification(
|
|
"☕ Break Time!",
|
|
f"Take a short break ({self.break_duration} minutes)\nCycle {
|
|
self.current_cycle
|
|
} completed! Total: {self.total_cycles_completed}",
|
|
)
|
|
|
|
# Schedule countdown notifications for break end
|
|
self._schedule_countdown_notifications(duration)
|
|
|
|
self.end_time = time.time() + duration
|
|
self.timer = threading.Timer(duration, self.timeout)
|
|
self.timer.start()
|
|
|
|
self.is_break = not self.is_break
|
|
|
|
def start(self, pomodoro_duration, break_duration, long_break_duration, count):
|
|
"""Start the Pomodoro timer with specified durations."""
|
|
self.stop()
|
|
self.pomodoro_duration = pomodoro_duration
|
|
self.break_duration = break_duration
|
|
self.long_break_duration = long_break_duration
|
|
self.count = count
|
|
self.remaining_till_long_break = count
|
|
self.current_cycle = 0
|
|
self.total_cycles_completed = 0
|
|
self.is_break = True
|
|
|
|
# Send initial notification
|
|
self._send_notification(
|
|
"🍅 Pomodoro Started!",
|
|
f"Timer started: {pomodoro_duration}min work, {break_duration}min break\n{
|
|
count
|
|
} cycles with {long_break_duration}min long breaks",
|
|
)
|
|
|
|
self.timeout()
|
|
|
|
def stop(self):
|
|
"""Stop the Pomodoro timer."""
|
|
if self.is_active():
|
|
self.timer.cancel()
|
|
self.timer = None
|
|
|
|
# Send stop notification
|
|
self._send_notification(
|
|
"⏹️ Pomodoro Stopped",
|
|
f"Timer stopped after {
|
|
self.total_cycles_completed
|
|
} completed cycles\nCurrent work period: {self.current_cycle}",
|
|
)
|
|
|
|
def is_active(self):
|
|
"""Check if the timer is currently active."""
|
|
return self.timer is not None
|
|
|
|
def get_status(self):
|
|
"""Get current timer status."""
|
|
if not self.is_active():
|
|
return "Inactive"
|
|
|
|
remaining_seconds = max(0, int(self.end_time - time.time()))
|
|
remaining_minutes = remaining_seconds // 60
|
|
remaining_seconds = remaining_seconds % 60
|
|
|
|
if self.is_break:
|
|
if self.remaining_till_long_break == self.count:
|
|
period_type = "Long Break"
|
|
else:
|
|
period_type = "Short Break"
|
|
else:
|
|
period_type = f"Work (Cycle {self.current_cycle})"
|
|
|
|
return f"{period_type} - {remaining_minutes:02d}:{remaining_seconds:02d} | Total: {self.total_cycles_completed}"
|
|
|
|
|
|
class PomodoroPlugin(PluginBase):
|
|
"""
|
|
Pomodoro Technique plugin for the launcher.
|
|
"""
|
|
|
|
# Default settings
|
|
DEFAULT_POMODORO_DURATION = 25
|
|
DEFAULT_BREAK_DURATION = 5
|
|
DEFAULT_LONG_BREAK_DURATION = 15
|
|
DEFAULT_POMODORO_COUNT = 4
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.display_name = "Pomodoro"
|
|
self.description = "Pomodoro Technique timer for productivity"
|
|
self.pomodoro = PomodoroTimer()
|
|
|
|
def initialize(self):
|
|
"""Initialize the Pomodoro plugin."""
|
|
self.set_triggers(["pomo", "pomo "])
|
|
|
|
def cleanup(self):
|
|
"""Cleanup the Pomodoro plugin."""
|
|
self.pomodoro.stop()
|
|
|
|
def query(self, query_string: str) -> List[Result]:
|
|
"""Process Pomodoro queries."""
|
|
results = []
|
|
|
|
if self.pomodoro.is_active():
|
|
# Timer is running - show status and stop option
|
|
status = self.pomodoro.get_status()
|
|
|
|
# Show current status
|
|
results.append(
|
|
Result(
|
|
title="Pomodoro Timer Active",
|
|
subtitle=status,
|
|
icon_markup=icons.timer_on,
|
|
action=lambda: None, # No action, just status display
|
|
relevance=1.0,
|
|
plugin_name=self.display_name,
|
|
data={"type": "status"},
|
|
)
|
|
)
|
|
|
|
# Show stop option
|
|
results.append(
|
|
Result(
|
|
title="Stop Pomodoro",
|
|
subtitle="Stop the current timer",
|
|
icon_markup=icons.timer_off,
|
|
action=lambda: self.pomodoro.stop(),
|
|
relevance=0.9,
|
|
plugin_name=self.display_name,
|
|
data={"type": "stop"},
|
|
)
|
|
)
|
|
else:
|
|
# Timer is not running - show start options
|
|
if not query_string.strip():
|
|
# No parameters - show default start option
|
|
results.append(
|
|
Result(
|
|
title="Start Pomodoro",
|
|
subtitle=f"{self.DEFAULT_POMODORO_DURATION} min work, {
|
|
self.DEFAULT_BREAK_DURATION
|
|
} min break, {
|
|
self.DEFAULT_LONG_BREAK_DURATION
|
|
} min long break, {self.DEFAULT_POMODORO_COUNT} cycles",
|
|
icon_markup=icons.timer_on,
|
|
action=lambda: self.pomodoro.start(
|
|
self.DEFAULT_POMODORO_DURATION,
|
|
self.DEFAULT_BREAK_DURATION,
|
|
self.DEFAULT_LONG_BREAK_DURATION,
|
|
self.DEFAULT_POMODORO_COUNT,
|
|
),
|
|
relevance=1.0,
|
|
plugin_name=self.display_name,
|
|
data={"type": "start_default"},
|
|
)
|
|
)
|
|
else:
|
|
# Parse parameters
|
|
tokens = query_string.strip().split()
|
|
|
|
# Validate parameters
|
|
if len(tokens) > 4 or not all(t.isdigit() for t in tokens):
|
|
results.append(
|
|
Result(
|
|
title="Invalid Parameters",
|
|
subtitle="Usage: [work_minutes] [break_minutes] [long_break_minutes] [cycle_count]",
|
|
icon_markup=icons.alert,
|
|
action=lambda: None,
|
|
relevance=0.5,
|
|
plugin_name=self.display_name,
|
|
data={"type": "error"},
|
|
)
|
|
)
|
|
else:
|
|
# Parse valid parameters
|
|
p = (
|
|
int(tokens[0])
|
|
if len(tokens) > 0
|
|
else self.DEFAULT_POMODORO_DURATION
|
|
)
|
|
b = (
|
|
int(tokens[1])
|
|
if len(tokens) > 1
|
|
else self.DEFAULT_BREAK_DURATION
|
|
)
|
|
lb = (
|
|
int(tokens[2])
|
|
if len(tokens) > 2
|
|
else self.DEFAULT_LONG_BREAK_DURATION
|
|
)
|
|
c = (
|
|
int(tokens[3])
|
|
if len(tokens) > 3
|
|
else self.DEFAULT_POMODORO_COUNT
|
|
)
|
|
|
|
results.append(
|
|
Result(
|
|
title="Start Pomodoro",
|
|
subtitle=f"{p} min work, {b} min break, {
|
|
lb
|
|
} min long break, {c} cycles",
|
|
icon_markup=icons.timer_on,
|
|
action=lambda _p=p, _b=b, _lb=lb, _c=c: self.pomodoro.start(
|
|
_p, _b, _lb, _c
|
|
),
|
|
relevance=1.0,
|
|
plugin_name=self.display_name,
|
|
data={"type": "start_custom", "params": [p, b, lb, c]},
|
|
)
|
|
)
|
|
|
|
return results
|