refactor(services): enhance TodoService with callbacks, UUID IDs, and UTF‑8 file handling

This commit is contained in:
sankalp committed 2026-07-12 09:33:41 +05:45
1 parent 51283125ef
commit afd826d0c0
6 files changed
+440 -44

No files matched your search

+43 -36
View File
@@ -1,30 +1,42 @@
# Standard library imports
import json
import os
import uuid
from datetime import datetime
from pathlib import Path
from fabric.core.service import Service
from fabric.utils import logger
# Fabric imports
from fabric.core.service import Property, Service
# Local imports
import shared.data as data
class TodoService(Service):
"""Service for managing persistent todo list with JSON storage"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
def __init__(self):
super().__init__()
self._todos = []
self._file_path = self._get_todos_file_path()
self._load_todos()
self._callbacks = []
def _notify_callbacks(self, event: str, data=None):
def add_callback(self, callback):
"""Add a callback function to be notified of changes"""
self._callbacks.append(callback)
def remove_callback(self, callback):
"""Remove a callback function"""
if callback in self._callbacks:
self._callbacks.remove(callback)
def _notify_callbacks(self, event_type, data=None):
"""Notify all registered callbacks of changes"""
for callback in self._callbacks:
try:
callback(event, data)
callback(event_type, data)
except Exception as e:
logger.error(f"Error in todo callback: {e}")
print(f"Error in todo callback: {e}")
def _get_todos_file_path(self):
"""Returns the path to the todos JSON file"""
@@ -36,23 +48,23 @@ class TodoService(Service):
"""Load todos from JSON file"""
try:
if self._file_path.exists():
with open(self._file_path, "r") as f:
with open(self._file_path, "r", encoding="utf-8") as f:
self._todos = json.load(f)
else:
self._todos = []
except Exception as e:
logger.error(f"Error loading todos: {e}")
print(f"Error loading todos: {e}")
self._todos = []
def _save_todos(self):
"""Save todos to JSON file"""
try:
with open(self._file_path, "w") as f:
with open(self._file_path, "w", encoding="utf-8") as f:
json.dump(self._todos, f, indent=2, ensure_ascii=False)
except Exception as e:
logger.error(f"Error saving todos: {e}")
print(f"Error saving todos: {e}")
@property
@Property(list, "readable")
def todos(self):
"""Get all todos"""
return self._todos.copy()
@@ -60,10 +72,10 @@ class TodoService(Service):
def add_todo(self, text: str, priority: str = "medium") -> dict:
"""Add a new todo item"""
todo = {
"id": os.urandom(8).hex(),
"id": str(uuid.uuid4()),
"text": text,
"completed": False,
"priority": priority,
"priority": priority, # low, medium, high
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
}
@@ -110,11 +122,7 @@ class TodoService(Service):
def set_priority(self, todo_id: str, priority: str) -> bool:
"""Set the priority of a todo item"""
valid_priorities = ["low", "medium", "high"]
if priority not in valid_priorities:
logger.warning(
f"Invalid priority '{priority}'. Must be one of {valid_priorities}"
)
if priority not in ["low", "medium", "high"]:
return False
for todo in self._todos:
@@ -127,6 +135,13 @@ class TodoService(Service):
return True
return False
def get_todo(self, todo_id: str) -> dict | None:
"""Get a specific todo by ID"""
for todo in self._todos:
if todo["id"] == todo_id:
return todo.copy()
return None
def clear_completed(self):
"""Remove all completed todos"""
initial_count = len(self._todos)
@@ -139,28 +154,20 @@ class TodoService(Service):
"""Get todo statistics"""
total = len(self._todos)
completed = sum(1 for todo in self._todos if todo["completed"])
pending = total - completed
return {
"total": total,
"completed": completed,
"active": total - completed,
"pending": pending,
"completion_rate": (completed / total * 100) if total > 0 else 0,
}
def add_callback(self, callback):
"""Add a callback for todo events"""
if callback not in self._callbacks:
self._callbacks.append(callback)
def remove_callback(self, callback):
"""Remove a callback"""
if callback in self._callbacks:
self._callbacks.remove(callback)
_todo_service = None
# Global service instance
todo_service = TodoService()
def get_todo_service() -> TodoService:
global _todo_service
if _todo_service is None:
_todo_service = TodoService()
return _todo_service
"""Get the global todo service instance"""
return todo_service
+2 -2
View File
@@ -24,12 +24,12 @@ class DismissLayer(Window):
class AppletWindow(PopupWindow):
_active_popup = None
def __init__(self, **kwargs):
def __init__(self, edge_margin: int = 0, **kwargs):
self._is_open = False
self._hide_timeout_id = None
self.dismiss_layer = DismissLayer(on_dismiss=self.toggle)
kwargs["keyboard_mode"] = "on-demand"
super().__init__(**kwargs)
super().__init__(edge_margin=edge_margin, **kwargs)
self.add_keybinding("escape", lambda *_: self.toggle())
def toggle(self, *_):
+6 -6
View File
@@ -2,8 +2,6 @@ from fabric.utils import Gdk, GLib, Gtk, logger
from fabric.widgets.wayland import WaylandWindow
from gi.repository import GtkLayerShell
EDGE_MARGIN = 0
def _get_monitor_geometry(widget: Gtk.Widget) -> tuple[int, int]:
screen = Gdk.Screen.get_default()
@@ -24,6 +22,7 @@ class PopupWindow(WaylandWindow):
parent: WaylandWindow | None = None,
pointing_to: Gtk.Widget | None = None,
margin: tuple[int, ...] | str = "0px 0px 0px 0px",
edge_margin: int = 0,
**kwargs,
):
super().__init__(**kwargs)
@@ -33,6 +32,7 @@ class PopupWindow(WaylandWindow):
self._pointing_widget = pointing_to
self._base_margin = self.extract_margin(margin)
self.margin = self._base_margin.values()
self.edge_margin = edge_margin
self.connect("notify::visible", self.do_update_handlers)
def get_coords_for_widget(self, widget: Gtk.Widget) -> tuple[int, int]:
@@ -163,15 +163,15 @@ class PopupWindow(WaylandWindow):
raw_margin = round((bar_left + coords_centered[0]) - (width / 2))
else:
raw_margin = round((parent_x_margin + coords_centered[0]) - (width / 2))
min_margin = EDGE_MARGIN
max_margin = monitor_width - width - EDGE_MARGIN
min_margin = self.edge_margin
max_margin = monitor_width - width - self.edge_margin
clamped = max(min_margin, min(raw_margin, max_margin))
position_margins = (0, 0, 0, clamped)
else:
raw_margin = round((parent_y_margin + coords_centered[1]) - (height / 2))
monitor_height = Gdk.Screen.get_default().get_height()
min_margin = EDGE_MARGIN
max_margin = monitor_height - height - EDGE_MARGIN
min_margin = self.edge_margin
max_margin = monitor_height - height - self.edge_margin
clamped = max(min_margin, min(raw_margin, max_margin))
position_margins = (clamped, 0, 0, 0)
+2
View File
@@ -15,3 +15,5 @@
font-family: "SF Pro Rounded";
}
@import url("widgets/pomodoro.css");
@import url("widgets/todo.css");
+122
View File
@@ -0,0 +1,122 @@
#pomodoro-popup {
background-color: @bg-a50;
border-radius: apply(radius-md);
box-shadow: inset 0 0 0 1px alpha(@text-dim, 0.4);
padding: 15px;
margin-right: 20px;
border: 1px solid @border-strong;
}
#pomodoro-container {
padding: 10px;
background-color: transparent;
}
#pomodoro-time {
font-size: 38px;
font-weight: 800;
color: @foreground;
font-family: "SF Pro Rounded";
}
#pomodoro-cycle {
font-size: 13px;
font-weight: 500;
color: @fg-a60;
font-family: "SF Pro Rounded";
}
#pomodoro-title {
font-size: 14px;
font-weight: 700;
color: @foreground;
font-family: "SF Pro Rounded";
}
/* Base progress bar color */
#pomodoro-progress {
background-color: transparent;
color: alpha(@primary, 0.3);
border-color: @primary;
}
/* Short break color */
#pomodoro-progress-short {
background-color: transparent;
color: alpha(@success, 0.3);
border-color: @success;
}
/* Long break color */
#pomodoro-progress-long {
background-color: transparent;
color: alpha(@error, 0.3);
border-color: @error;
}
#pomodoro-phase-btn {
background-color: @fg-a18;
color: alpha(@theme_fg_color, 0.7);
border-radius: apply(radius-sm);
padding: 5px 14px;
font-weight: 600;
font-size: 12px;
}
#pomodoro-phase-btn:hover {
background-color: @fg-a25;
color: @theme_fg_color;
}
#pomodoro-phase-btn.active {
background-color: @primary;
color: white;
}
#pomodoro-start-btn {
background-color: @primary;
color: white;
border-radius: apply(radius-sm);
padding: 6px 20px;
font-weight: bold;
font-size: 14px;
}
#pomodoro-start-btn:hover {
background-color: alpha(@primary, 0.8);
}
#pomodoro-reset-btn, #pomodoro-secondary-btn {
background-color: @fg-a18;
color: @theme_fg_color;
border-radius: apply(radius-sm);
padding: 6px 15px;
font-weight: 600;
font-size: 14px;
}
#pomodoro-reset-btn:hover, #pomodoro-secondary-btn:hover {
background-color: @fg-a25;
}
#pomodoro-spin-box {
background-color: @fg-a18;
border-radius: apply(radius-sm);
padding: 2px;
}
#pomodoro-spin-btn {
background: transparent;
border: none;
box-shadow: none;
padding: 2px 4px;
min-height: 16px;
color: @theme_fg_color;
border-radius: apply(radius-sm);
font-size: 10px;
}
#pomodoro-spin-btn:hover {
background-color: @fg-a25;
}
+265
View File
@@ -0,0 +1,265 @@
/* === Main Todo List === */
#todo-list-menu {
background-color: @bg-a50;
border-radius: apply(radius-md);
box-shadow: inset 0 0 0 1px alpha(@text-dim, 0.4);
padding: 15px;
margin-right: 20px;
border: 1px solid @border-strong;
}
#todo-main-container {
padding: 5px;
background-color: transparent;
}
#todo-header {
padding: 0;
margin-bottom: 5px;
}
#todo-title {
font-size: 16px;
font-weight: 700;
color: @foreground;
font-family: "SF Pro Rounded";
}
#todo-stats {
font-size: 12px;
color: @fg-a60;
font-family: "SF Pro Rounded";
}
/* === Add Task Section === */
#todo-add-section {
background-color: @fg-a18;
border-radius: apply(radius-sm);
padding: 8px 10px;
}
#new-todo-entry {
background: transparent;
border: none;
color: @foreground;
font-size: 13px;
font-family: "SF Pro Rounded";
min-height: 24px;
}
#new-todo-entry placeholder {
color: @fg-a40;
}
#add-todo-button {
background-color: @primary;
border-radius: apply(radius-full);
padding: 6px;
min-width: 24px;
min-height: 24px;
}
#add-todo-button:hover {
background-color: alpha(@primary, 0.8);
}
/* === Todo Items === */
#todos-container {
padding: 0;
}
#todo-item {
background-color: transparent;
border-radius: apply(radius-sm);
padding: 8px 6px;
transition: background-color 0.2s ease;
}
#todo-item:hover {
background-color: alpha(@fg-a10, 0.3);
}
#todo-divider {
min-height: 1px;
background-color: alpha(@fg-a20, 0.5);
margin: 0 4px;
}
/* === Progress Ring === */
#todo-progress-ring {
background-color: transparent;
color: alpha(@foreground, 0.15);
border-color: @foreground;
}
/* === Checkbox === */
#todo-checkbox {
background: transparent;
border: none;
box-shadow: none;
padding: 0;
min-width: 18px;
min-height: 18px;
}
#todo-checkbox:hover {
background-color: alpha(@fg-a18, 0.5);
border-radius: apply(radius-sm);
}
/* === Task Text === */
#todo-text {
font-size: 14px;
font-weight: 500;
color: @foreground;
font-family: "SF Pro Rounded";
}
/* === Options Button === */
#todo-options-btn {
background: transparent;
border: none;
box-shadow: none;
padding: 4px 8px;
min-width: 24px;
min-height: 24px;
font-size: 16px;
color: @fg-a40;
opacity: 0;
transition: opacity 0.2s ease;
}
#todo-item:hover #todo-options-btn {
opacity: 1;
}
#todo-options-btn:hover {
color: @foreground;
background-color: alpha(@fg-a18, 0.5);
border-radius: apply(radius-sm);
}
/* === Clear + Footer === */
#clear-completed-button {
background: transparent;
border: none;
font-size: 12px;
color: @fg-a40;
padding: 4px;
}
#clear-completed-button:hover {
color: @foreground;
}
#todo-footer {
font-size: 11px;
color: @fg-a40;
font-family: "SF Pro Rounded";
margin-top: 4px;
}
/* === Options Popover === */
#todo-options-popover {
background-color: @bg-a50;
border-radius: apply(radius-md);
box-shadow: inset 0 0 0 1px alpha(@text-dim, 0.4);
padding: 10px;
margin-right: 20px;
border: 1px solid @border-strong;
min-width: 340px;
}
#todo-options-container {
background-color: transparent;
}
#todo-opt-title {
font-size: 14px;
font-weight: 700;
color: @foreground;
font-family: "SF Pro Rounded";
}
#todo-opt-subtitle {
font-size: 11px;
color: @fg-a60;
font-family: "SF Pro Rounded";
}
/* === Color Swatches === */
#todo-color-swatch {
background-color: @fg-a18;
border-radius: apply(radius-full);
padding: 0;
min-width: 28px;
min-height: 28px;
border: 2px solid transparent;
transition: border-color 0.15s ease;
}
#todo-color-swatch:hover {
border-color: alpha(@foreground, 0.3);
}
#todo-color-swatch.selected {
border-color: @foreground;
box-shadow: 0 0 0 2px alpha(@foreground, 0.2);
}
/* === Deadline Pill === */
#todo-deadline-pill {
background-color: @fg-a18;
border-radius: apply(radius-full);
padding: 6px 12px;
}
#todo-deadline-value {
font-size: 12px;
color: @foreground;
font-family: "SF Pro Rounded";
min-width: 70px;
}
/* === Stepper (reuse pomodoro style) === */
#todo-spin-box {
background-color: @fg-a10;
border-radius: apply(radius-sm);
padding: 2px;
}
#todo-spin-btn {
background: transparent;
border: none;
box-shadow: none;
padding: 2px 4px;
min-height: 14px;
color: @theme_fg_color;
border-radius: apply(radius-sm);
font-size: 8px;
}
#todo-spin-btn:hover {
background-color: @fg-a25;
}
#todo-spin-value {
font-size: 12px;
color: @foreground;
font-family: "SF Pro Rounded";
}
/* === Preset Buttons === */
#todo-preset-btn {
background-color: @fg-a18;
border-radius: apply(radius-full);
padding: 6px 12px;
font-size: 11px;
color: @foreground;
font-family: "SF Pro Rounded";
}
#todo-preset-btn:hover {
background-color: @fg-a25;
}