Clide is being rebuilt as a Flutter desktop app. The Python Textual implementation moves wholesale into legacy/ rather than being deleted: its pane model, panel set, git skills, and panel communication design are real thought that should remain readable next to the new code while the rebuild finds its shape. Git's rename tracking preserves history, so `git log -- legacy/` still works. The Flutter rebuild lives at the repo root alongside a Go sidecar (the architecture claudian was heading toward, which folds into clide as a core component rather than a separate plugin project). Bootstrap of the new shape lands in subsequent commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
164 lines
4.4 KiB
Python
164 lines
4.4 KiB
Python
"""TODOs controller for tracking TODO comments."""
|
|
|
|
from pathlib import Path
|
|
|
|
from textual.message import Message
|
|
|
|
from clide.controllers.base import controller
|
|
from clide.models.todos import (
|
|
ProjectTodoItem,
|
|
TodoItem,
|
|
TodosState,
|
|
TodosSummary,
|
|
TodoType,
|
|
)
|
|
from clide.services.todo_scanner import TodoScanner
|
|
|
|
|
|
@controller
|
|
class TodosController:
|
|
"""Controller for TODO/FIXME comment tracking."""
|
|
|
|
class TodosUpdated(Message):
|
|
"""Emitted when TODOs list is updated."""
|
|
|
|
def __init__(
|
|
self,
|
|
items: list[TodoItem],
|
|
project_items: list[ProjectTodoItem],
|
|
summary: TodosSummary,
|
|
) -> None:
|
|
self.items = items
|
|
self.project_items = project_items
|
|
self.summary = summary
|
|
super().__init__()
|
|
|
|
class TodoSelected(Message):
|
|
"""Emitted when a TODO is selected."""
|
|
|
|
def __init__(self, item: TodoItem) -> None:
|
|
self.item = item
|
|
super().__init__()
|
|
|
|
def __init__(self, project_path: Path) -> None:
|
|
self._scanner = TodoScanner(project_path)
|
|
self._state = TodosState()
|
|
|
|
@property
|
|
def state(self) -> TodosState:
|
|
"""Get TODOs state."""
|
|
return self._state
|
|
|
|
@property
|
|
def items(self) -> list[TodoItem]:
|
|
"""Get list of code TODO items."""
|
|
return self._state.items
|
|
|
|
@property
|
|
def project_items(self) -> list[ProjectTodoItem]:
|
|
"""Get list of project TODO items from TODO.md."""
|
|
return self._state.project_items
|
|
|
|
@property
|
|
def summary(self) -> TodosSummary:
|
|
"""Get TODOs summary."""
|
|
return self._state.summary
|
|
|
|
@property
|
|
def total_count(self) -> int:
|
|
"""Get total code TODO count."""
|
|
return self._state.summary.total
|
|
|
|
@property
|
|
def project_count(self) -> int:
|
|
"""Get total project TODO count."""
|
|
return self._state.summary.project_total
|
|
|
|
async def refresh(
|
|
self,
|
|
) -> tuple[list[TodoItem], list[ProjectTodoItem], TodosSummary]:
|
|
"""Refresh TODOs from project.
|
|
|
|
Returns:
|
|
Tuple of (code items, project items, summary)
|
|
"""
|
|
items, project_items, summary = await self._scanner.scan()
|
|
|
|
self._state.items = items
|
|
self._state.project_items = project_items
|
|
self._state.summary = summary
|
|
|
|
return items, project_items, summary
|
|
|
|
def filter_by_type(self, todo_type: TodoType | None) -> list[TodoItem]:
|
|
"""Filter TODOs by type.
|
|
|
|
Args:
|
|
todo_type: Type to filter by, or None for all
|
|
|
|
Returns:
|
|
Filtered list of TODOs
|
|
"""
|
|
self._state.filter_type = todo_type
|
|
|
|
if todo_type is None:
|
|
return self._state.items
|
|
|
|
return self._state.items_by_type(todo_type)
|
|
|
|
def get_items_for_file(self, path: Path) -> list[TodoItem]:
|
|
"""Get TODOs for a specific file.
|
|
|
|
Args:
|
|
path: File path
|
|
|
|
Returns:
|
|
List of TODOs for that file
|
|
"""
|
|
return self._state.items_for_file(path)
|
|
|
|
def select_item(self, index: int) -> TodoItem | None:
|
|
"""Select a TODO by index.
|
|
|
|
Args:
|
|
index: Item index
|
|
|
|
Returns:
|
|
Selected item or None
|
|
"""
|
|
if 0 <= index < len(self._state.items):
|
|
self._state.selected_index = index
|
|
return self._state.items[index]
|
|
return None
|
|
|
|
def toggle_group_by_file(self) -> bool:
|
|
"""Toggle grouping by file.
|
|
|
|
Returns:
|
|
New group_by_file value
|
|
"""
|
|
self._state.group_by_file = not self._state.group_by_file
|
|
return self._state.group_by_file
|
|
|
|
def get_grouped_items(self) -> dict[Path, list[TodoItem]]:
|
|
"""Get TODOs grouped by file.
|
|
|
|
Returns:
|
|
Dictionary mapping file paths to TODO lists
|
|
"""
|
|
grouped: dict[Path, list[TodoItem]] = {}
|
|
for item in self._state.items:
|
|
if item.file_path not in grouped:
|
|
grouped[item.file_path] = []
|
|
grouped[item.file_path].append(item)
|
|
return grouped
|
|
|
|
async def scan(self) -> tuple[list[TodoItem], list[ProjectTodoItem]]:
|
|
"""Scan for TODOs and return items.
|
|
|
|
Returns:
|
|
Tuple of (code items, project items)
|
|
"""
|
|
items, project_items, _ = await self.refresh()
|
|
return items, project_items
|