# Standard library imports from datetime import datetime # Fabric imports from fabric.utils import get_relative_path from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox from fabric.widgets.entry import Entry from fabric.widgets.label import Label from fabric.widgets.scrolledwindow import ScrolledWindow from fabric.widgets.svg import Svg from gi.repository import GLib # Local imports from services.todo import todo_service from widgets.mousecapture import MouseCapture from widgets.wayland import WaylandWindow as Window class TodoItem(Box): """Individual todo item widget""" def __init__(self, todo_data, todo_list_widget, **kwargs): self.todo_data = todo_data self.todo_list_widget = todo_list_widget self.editing = False super().__init__( name="todo-item", orientation="h", spacing=8, style_classes=["menu"], **kwargs, ) self._build_ui() def _build_ui(self): """Build the todo item UI""" # Checkbox for completion - using SVG icons checkbox_icon = ( "checkbox-check.svg" if self.todo_data["completed"] else "checkbox-uncheck.svg" ) self.checkbox_icon = Svg( name="todo-checkbox-icon", size=24, svg_file=get_relative_path( "../../config/assets/icons/todo/" + checkbox_icon ), ) self.checkbox = Button( name="todo-checkbox", child=self.checkbox_icon, on_clicked=self._toggle_completion, ) # Todo text (can be converted to entry for editing) text_content = self.todo_data["text"] if self.todo_data["completed"]: text_content = f"{text_content}" self.text_label = Label( markup=text_content, name="todo-text", h_align="start", h_expand=True, line_wrap="word-char", style_classes=( ["title-widget"] if not self.todo_data["completed"] else ["status-label"] ), ) # Date/time label created_at = datetime.fromisoformat(self.todo_data["created_at"]) date_text = created_at.strftime("%b %d, %Y at %I:%M %p") self.date_label = Label( label=date_text, name="todo-date", h_align="start", style_classes=["todo-date-text"], ) self.text_entry = Entry( name="todo-text-entry", text=self.todo_data["text"], h_expand=True, visible=False, ) self.text_entry.connect("activate", self._save_edit) # Priority indicator # priority_symbols = {"high": "🔴", "medium": "🟡", "low": "🟢"} # # self.priority_label = Label( # label=priority_symbols.get(self.todo_data["priority"], "🟡"), # name="todo-priority-label", # ) # self.priority_button = Button( # name="todo-priority", # size=(20, 20), # child=self.priority_label, # on_clicked=self._cycle_priority, # ) # # Edit button - using SVG icon self.edit_icon = Svg( name="todo-edit-icon", size=12, svg_file=get_relative_path("../../config/assets/icons/todo/edit.svg"), ) self.edit_button = Button( name="todo-edit", child=self.edit_icon, on_clicked=self._start_edit, ) # Delete button - using SVG icon self.delete_icon = Svg( name="todo-delete-icon", size=12, svg_file=get_relative_path( "../../config/assets/icons/todo/delete-symbolic.svg" ), ) self.delete_button = Button( name="todo-delete", child=self.delete_icon, on_clicked=self._delete_todo, ) # Text container that switches between label and entry self.text_container = Box( orientation="v", h_expand=True, children=[ Box(orientation="h", children=[self.text_label]), self.date_label, ], ) self.children = [ self.checkbox, self.text_container, # self.priority_button, self.edit_button, self.delete_button, ] def _toggle_completion(self, *_): """Toggle todo completion status""" todo_service.toggle_todo(self.todo_data["id"]) def _cycle_priority(self, *_): """Cycle through priority levels""" priorities = ["low", "medium", "high"] current_index = priorities.index(self.todo_data["priority"]) new_priority = priorities[(current_index + 1) % len(priorities)] todo_service.set_priority(self.todo_data["id"], new_priority) def _start_edit(self, *_): """Start editing the todo text""" if self.editing: return self.editing = True self.text_container.children = [ Box(orientation="h", children=[self.text_entry]), self.date_label, ] self.text_entry.set_visible(True) self.text_entry.grab_focus() self.text_entry.set_position(-1) # Move cursor to end def _save_edit(self, *_): """Save the edited todo text""" if not self.editing: return new_text = self.text_entry.get_text().strip() if new_text: todo_service.edit_todo(self.todo_data["id"], new_text) self._cancel_edit() def _cancel_edit(self): """Cancel editing and revert to label""" self.editing = False self.text_container.children = [ Box(orientation="h", children=[self.text_label]), self.date_label, ] self.text_entry.set_visible(False) def _delete_todo(self, *_): """Delete this todo""" todo_service.delete_todo(self.todo_data["id"]) def update_from_data(self, todo_data): """Update the widget from new todo data""" self.todo_data = todo_data # Update checkbox icon by recreating it checkbox_icon = ( "checkbox-check.svg" if todo_data["completed"] else "checkbox-uncheck.svg" ) new_checkbox_icon = Svg( name="todo-checkbox-icon", size=20, svg_file=get_relative_path( "../../config/assets/icons/todo/" + checkbox_icon ), ) self.checkbox.set_child(new_checkbox_icon) self.checkbox_icon = new_checkbox_icon # Update text and styling with markup text_content = todo_data["text"] if todo_data["completed"]: text_content = f"{text_content}" self.text_label.set_markup(text_content) self.text_label.style_classes = ( ["title-widget"] if not todo_data["completed"] else ["status-label"] ) # Update date/time created_at = datetime.fromisoformat(todo_data["created_at"]) date_text = created_at.strftime("%b %d, %Y at %I:%M %p") self.date_label.set_label(date_text) class TodoListWidget(Window): """Main todo list widget window""" def __init__(self, **kwargs): super().__init__( title="modus-todo", anchor="top right", margin="2px 10px 0px 0px", exclusivity="auto", keyboard_mode="on-demand", name="todo-list-window", visible=False, # Back to hidden by default **kwargs, ) self.todo_items = {} # Maps todo IDs to TodoItem widgets # Register callback with todo service todo_service.add_callback(self._on_todo_event) self._build_ui() self._refresh_todos() # Add keybinding for escape self.add_keybinding("Escape", self.hide_todo_list) def _build_ui(self): """Build the main UI""" # Header with title and stats self.stats_label = Label( label="", name="todo-stats", style_classes=["status-label"], h_align="start", ) self.header = Box( name="todo-header", orientation="v", children=[ Label( label="Todo List", name="todo-title", style_classes=["title"], h_align="start", ), self.stats_label, ], ) # Add new todo section self.new_todo_entry = Entry( name="new-todo-entry", placeholder_text="Add a new task...", h_expand=True, ) self.new_todo_entry.connect("activate", self._add_todo) # Add button - using SVG icon self.add_icon = Svg( name="add-todo-icon", size=12, svg_file=get_relative_path( "../../config/assets/icons/todo/plus-symbolic.svg" ), ) self.add_button = Button( name="add-todo-button", child=self.add_icon, on_clicked=self._add_todo, ) self.add_section = Box( name="todo-add-section", orientation="h", spacing=8, style_classes=["menu"], children=[ self.new_todo_entry, self.add_button, ], ) # Todo items container self.todos_container = Box( name="todos-container", orientation="v", spacing=4, ) # Scrolled window for todos self.scrolled = ScrolledWindow( name="todos-scrolled", min_content_height=300, max_content_height=500, min_content_width=400, child=self.todos_container, policy="automatic", v_expand=True, # Allow vertical expansion ) # Clear completed button self.clear_button = Button( name="clear-completed-button", label="Clear Completed", style_classes=["status-label"], on_clicked=self._clear_completed, ) # Main container self.main_container = Box( name="todo-main-container", orientation="v", spacing=8, style_classes=["menu"], children=[ self.header, self.add_section, self.scrolled, self.clear_button, ], ) self.children = [self.main_container] def _add_todo(self, *_): """Add a new todo""" text = self.new_todo_entry.get_text().strip() if text: todo_service.add_todo(text) self.new_todo_entry.set_text("") def _clear_completed(self, *_): """Clear all completed todos""" todo_service.clear_completed() def _on_todo_event(self, event_type, data=None): """Handle todo service events via callback""" if event_type == "todos-changed": GLib.idle_add(self._refresh_todos) elif event_type == "todo-added": GLib.idle_add(self._refresh_todos) elif event_type == "todo-deleted": GLib.idle_add(self._refresh_todos) elif event_type in ["todo-toggled", "todo-edited", "todo-priority-changed"]: if data and data["id"] in self.todo_items: GLib.idle_add( lambda: self.todo_items[data["id"]].update_from_data(data) ) GLib.idle_add(self._update_stats) def _refresh_todos(self, *_): """Refresh the entire todo list""" # Clear existing items self.todo_items.clear() self.todos_container.children = [] # Get all todos todos = todo_service.todos # Sort todos: incomplete first, then by priority, then by creation date def sort_key(todo): priority_order = {"high": 0, "medium": 1, "low": 2} return ( todo["completed"], # False (incomplete) comes before True (completed) priority_order.get(todo["priority"], 1), todo["created_at"], ) sorted_todos = sorted(todos, key=sort_key) # Create todo item widgets for todo in sorted_todos: todo_item = TodoItem(todo, self) self.todo_items[todo["id"]] = todo_item # Update container children self.todos_container.children = list(self.todo_items.values()) # Update stats self._update_stats() def _update_stats(self): """Update the statistics display""" stats = todo_service.get_stats() stats_text = f"{stats['pending']} pending, {stats['completed']} completed" self.stats_label.set_label(stats_text) def set_visible(self, visible): """Override set_visible for debugging""" super().set_visible(visible) def hide_todo_list(self, *_): """Hide the todo list""" if hasattr(self, "_mousecapture_parent"): self._mousecapture_parent.toggle_mousecapture() self.set_visible(False) def _init_mousecapture(self, mousecapture): """Initialize mousecapture parent reference""" self._mousecapture_parent = mousecapture def destroy(self): """Clean up when destroyed""" # Remove callback from todo service todo_service.remove_callback(self._on_todo_event) super().destroy() class TodoListCapture(MouseCapture): """MouseCapture wrapper for the todo list""" def __init__(self, **kwargs): super().__init__( layer="top", child_window=TodoListWidget(), **kwargs, )