#
tokens: 33341/50000 20/21 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 2. Use http://codebase.md/mryanmyn/task-manager-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── app
│   ├── __init__.py
│   ├── api
│   │   ├── __init__.py
│   │   ├── api.py
│   │   └── cli.py
│   ├── core
│   │   ├── __init__.py
│   │   ├── plan_manager.py
│   │   └── task_manager.py
│   └── ui
│       ├── __init__.py
│       ├── input_handler.py
│       ├── terminal_ui.py
│       └── ui_components.py
├── img.png
├── main.py
├── mcp_guidelines
│   ├── __init__.py
│   └── llms_full.txt
├── mcp_server_fixed.py
├── MCP-README.md
├── pyproject.toml
├── README.md
├── setup.py
└── tests
    ├── __init__.py
    └── test_mcp_server.py
```

# Files

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Terminal Task Tracker
  2 | 
  3 | A terminal-based task tracking application with a three-pane layout for managing tasks and project plans.
  4 | 
  5 |  # Image
  6 | 
  7 | ![Terminal Task Tracker](https://github.com/MrYanMYN/task-manager-mcp/blob/master/img.png?raw=true)
  8 | 
  9 | ## Features
 10 | 
 11 | - Three-pane terminal UI:
 12 |   - Task list (top left)
 13 |   - Task details (top right)
 14 |   - Project plan (bottom, full width)
 15 | - Task management:
 16 |   - Create, view, edit, and delete tasks
 17 |   - Set priorities and status
 18 |   - Add detailed descriptions
 19 | - Project plan management:
 20 |   - Define high-level project steps
 21 |   - Track step completion
 22 |   - Reorder steps
 23 | - Complete API for programmatic access
 24 | - Command-line interface for scripting
 25 | - Data persistence
 26 | 
 27 | ## Installation
 28 | 
 29 | ```bash
 30 | # Clone the repository
 31 | git clone https://github.com/yourusername/terminal-task-tracker.git
 32 | cd terminal-task-tracker
 33 | 
 34 | # Install dependencies
 35 | pip install -e .
 36 | ```
 37 | 
 38 | ## Usage
 39 | 
 40 | ### Terminal UI
 41 | 
 42 | To start the terminal UI:
 43 | 
 44 | ```bash
 45 | python -m main.py
 46 | ```
 47 | 
 48 | Key bindings:
 49 | - `Tab`: Cycle between windows
 50 | - `Up/Down`: Navigate lists
 51 | - `Enter`: Select task (in task list)
 52 | - `n`: New item (in task list or plan)
 53 | - `e`: Edit item
 54 | - `d`: Delete item
 55 | - `Space`: Toggle completion (in plan)
 56 | - `Esc`: Exit
 57 | 
 58 | ### Command-line Interface
 59 | 
 60 | The CLI provides access to all functionality:
 61 | 
 62 | ```bash
 63 | # List all tasks
 64 | python -m app.api.cli task list
 65 | 
 66 | # Add a new task
 67 | python -m app.api.cli task add "Implement feature X" --description "Details about feature X" --priority 2
 68 | 
 69 | # Mark a plan step as completed
 70 | python -m app.api.cli plan toggle STEP_ID
 71 | 
 72 | # Export data to JSON
 73 | python -m app.api.cli export data.json
 74 | ```
 75 | 
 76 | ### API Usage
 77 | 
 78 | ```python
 79 | from app.core.task_manager import TaskManager
 80 | from app.core.plan_manager import PlanManager
 81 | from app.api.api import TaskTrackerAPI
 82 | 
 83 | # Initialize managers
 84 | task_manager = TaskManager("tasks.json")
 85 | plan_manager = PlanManager("plan.json")
 86 | 
 87 | # Create API
 88 | api = TaskTrackerAPI(task_manager, plan_manager)
 89 | 
 90 | # Add a task
 91 | task = api.add_task("Implement feature X", "Details about feature X", priority=2)
 92 | 
 93 | # Add a plan step
 94 | step = api.add_plan_step("Design architecture for shared operations module")
 95 | 
 96 | # Mark step as completed
 97 | api.toggle_plan_step(step["id"])
 98 | 
 99 | # Save data
100 | api.save_all()
101 | ```
102 | 
103 | ## Project Structure
104 | 
105 | ```
106 | terminal-task-tracker/
107 | ├── app/
108 | │   ├── __init__.py
109 | │   ├── core/               # Business logic
110 | │   │   ├── __init__.py
111 | │   │   ├── task_manager.py
112 | │   │   └── plan_manager.py
113 | │   ├── ui/                 # Terminal UI
114 | │   │   ├── __init__.py
115 | │   │   ├── terminal_ui.py
116 | │   │   ├── ui_components.py
117 | │   │   └── input_handler.py
118 | │   └── api/                # API and CLI
119 | │       ├── __init__.py
120 | │       ├── api.py
121 | │       └── cli.py
122 | ├── main.py                 # Main application entry point
123 | └── README.md
124 | ```
125 | 
126 | ## Data Storage
127 | 
128 | By default, data is stored in the `~/.tasktracker` directory:
129 | - `tasks.json`: Tasks data
130 | - `plan.json`: Project plan data
131 | - `notes.json`: Notes data
132 | 
133 | ## License
134 | 
135 | MIT
```

--------------------------------------------------------------------------------
/mcp_guidelines/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
```

--------------------------------------------------------------------------------
/app/api/__init__.py:
--------------------------------------------------------------------------------

```python
1 | # API and CLI module
```

--------------------------------------------------------------------------------
/app/ui/__init__.py:
--------------------------------------------------------------------------------

```python
1 | # Terminal UI module
```

--------------------------------------------------------------------------------
/app/core/__init__.py:
--------------------------------------------------------------------------------

```python
1 | # Core business logic module
```

--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """
2 | Tests for TaskTracker MCP package.
3 | """
```

--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------

```python
1 | # Terminal Task Tracker application package
```

--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------

```python
 1 | from setuptools import setup, find_packages
 2 | 
 3 | setup(
 4 |     name="terminal-task-tracker",
 5 |     version="0.1.0",
 6 |     description="A terminal-based task tracking application with a three-pane layout",
 7 |     author="Your Name",
 8 |     author_email="[email protected]",
 9 |     packages=find_packages(),
10 |     entry_points={
11 |         "console_scripts": [
12 |             "tasktracker=main:main",
13 |         ],
14 |     },
15 |     python_requires=">=3.6",
16 |     classifiers=[
17 |         "Development Status :: 3 - Alpha",
18 |         "Environment :: Console :: Curses",
19 |         "Intended Audience :: Developers",
20 |         "Intended Audience :: End Users/Desktop",
21 |         "License :: OSI Approved :: MIT License",
22 |         "Programming Language :: Python :: 3",
23 |         "Programming Language :: Python :: 3.6",
24 |         "Programming Language :: Python :: 3.7",
25 |         "Programming Language :: Python :: 3.8",
26 |         "Programming Language :: Python :: 3.9",
27 |         "Topic :: Office/Business :: Scheduling",
28 |         "Topic :: Utilities"
29 |     ],
30 | )
```

--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python3
 2 | """
 3 | Terminal Task Tracker - Main Application Entry Point
 4 | 
 5 | A terminal-based task tracking application with a three-pane layout
 6 | for managing tasks and project plans.
 7 | """
 8 | 
 9 | import os
10 | import sys
11 | 
12 | from app.core.task_manager import TaskManager
13 | from app.core.plan_manager import PlanManager
14 | from app.api.api import TaskTrackerAPI
15 | from app.ui.terminal_ui import TerminalUI
16 | 
17 | 
18 | def main():
19 |     """Main application entry point."""
20 |     try:
21 |         # Create data directory if it doesn't exist
22 |         home_dir = os.path.expanduser("~")
23 |         data_dir = os.path.join(home_dir, ".tasktracker")
24 |         os.makedirs(data_dir, exist_ok=True)
25 |         
26 |         # Initialize managers
27 |         task_manager = TaskManager()
28 |         plan_manager = PlanManager()
29 |         
30 |         # Create API
31 |         api = TaskTrackerAPI(task_manager, plan_manager)
32 |         
33 |         # Run terminal UI
34 |         ui = TerminalUI(api)
35 |         ui.run()
36 |         
37 |         # Save data on exit
38 |         api.save_all()
39 |         
40 |         return 0
41 |     except Exception as e:
42 |         print(f"Error: {str(e)}", file=sys.stderr)
43 |         return 1
44 | 
45 | 
46 | if __name__ == "__main__":
47 |     sys.exit(main())
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [build-system]
 2 | requires = ["hatchling"]
 3 | build-backend = "hatchling.build"
 4 | 
 5 | [project]
 6 | name = "tasktracker-mcp"
 7 | version = "0.1.0"
 8 | description = "A terminal-based task tracking application with MCP integration"
 9 | readme = "README.md"
10 | authors = [
11 |     {name = "Your Name", email = "[email protected]"},
12 | ]
13 | license = {text = "MIT"}
14 | requires-python = ">=3.8"
15 | classifiers = [
16 |     "Programming Language :: Python :: 3",
17 |     "Programming Language :: Python :: 3.8",
18 |     "Programming Language :: Python :: 3.9",
19 |     "Programming Language :: Python :: 3.10",
20 |     "Programming Language :: Python :: 3.11",
21 |     "License :: OSI Approved :: MIT License",
22 |     "Operating System :: OS Independent",
23 | ]
24 | dependencies = [
25 |     "mcp>=0.1.0",
26 | ]
27 | 
28 | [project.optional-dependencies]
29 | dev = [
30 |     "ruff>=0",
31 |     "pytest>=7.0.0",
32 |     "pytest-cov>=4.0.0",
33 |     "pyright>=0",
34 | ]
35 | 
36 | [tool.hatch.build.targets.wheel]
37 | packages = ["app"]
38 | 
39 | [tool.ruff]
40 | line-length = 88
41 | target-version = "py38"
42 | 
43 | [tool.ruff.lint]
44 | select = [
45 |     "E",   # pycodestyle errors
46 |     "F",   # pyflakes
47 |     "I",   # isort
48 | ]
49 | 
50 | [tool.pyright]
51 | include = ["app", "mcp_server.py"]
52 | typeCheckingMode = "basic"
53 | reportMissingTypeStubs = false
54 | 
55 | [tool.pytest.ini_options]
56 | testpaths = ["tests"]
57 | python_files = "test_*.py"
58 | 
59 | [project.scripts]
60 | tasktracker-mcp = "mcp_server:main"
```

--------------------------------------------------------------------------------
/tests/test_mcp_server.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the TaskTracker MCP server.
  3 | """
  4 | import json
  5 | import pytest
  6 | from unittest.mock import MagicMock, patch
  7 | 
  8 | # Import the MCP server
  9 | import sys
 10 | import os
 11 | 
 12 | # Add the parent directory to the path
 13 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
 14 | 
 15 | import mcp_server
 16 | 
 17 | 
 18 | @pytest.fixture
 19 | def mock_api():
 20 |     """Create a mock TaskTrackerAPI for testing."""
 21 |     mock = MagicMock()
 22 |     
 23 |     # Set up return values for get_all_tasks
 24 |     mock.get_all_tasks.return_value = [
 25 |         {
 26 |             "id": "test-task-1",
 27 |             "title": "Test Task 1",
 28 |             "description": "Description for test task 1",
 29 |             "priority": 1,
 30 |             "status": "not_started",
 31 |             "created_at": "2025-04-08T00:00:00",
 32 |             "updated_at": "2025-04-08T00:00:00"
 33 |         }
 34 |     ]
 35 |     
 36 |     # Set up return values for get_task
 37 |     mock.get_task.return_value = {
 38 |         "id": "test-task-1",
 39 |         "title": "Test Task 1",
 40 |         "description": "Description for test task 1",
 41 |         "priority": 1,
 42 |         "status": "not_started",
 43 |         "created_at": "2025-04-08T00:00:00",
 44 |         "updated_at": "2025-04-08T00:00:00"
 45 |     }
 46 |     
 47 |     # Set up return values for add_task
 48 |     mock.add_task.return_value = {
 49 |         "id": "new-task-1",
 50 |         "title": "New Task",
 51 |         "description": "Description for new task",
 52 |         "priority": 1,
 53 |         "status": "not_started",
 54 |         "created_at": "2025-04-08T00:00:00",
 55 |         "updated_at": "2025-04-08T00:00:00"
 56 |     }
 57 |     
 58 |     return mock
 59 | 
 60 | 
 61 | @patch("mcp_server.api")
 62 | def test_get_all_tasks(mock_api_module, mock_api):
 63 |     """Test the get_all_tasks resource."""
 64 |     # Set the mock API
 65 |     mock_api_module.get_all_tasks.return_value = mock_api.get_all_tasks.return_value
 66 |     
 67 |     # Call the function
 68 |     result = mcp_server.get_all_tasks()
 69 |     
 70 |     # Assert the result
 71 |     expected = json.dumps(mock_api.get_all_tasks.return_value, indent=2)
 72 |     assert result == expected
 73 |     mock_api_module.get_all_tasks.assert_called_once()
 74 | 
 75 | 
 76 | @patch("mcp_server.api")
 77 | def test_get_task(mock_api_module, mock_api):
 78 |     """Test the get_task resource."""
 79 |     # Set the mock API
 80 |     mock_api_module.get_task.return_value = mock_api.get_task.return_value
 81 |     
 82 |     # Call the function
 83 |     result = mcp_server.get_task("test-task-1")
 84 |     
 85 |     # Assert the result
 86 |     expected = json.dumps(mock_api.get_task.return_value, indent=2)
 87 |     assert result == expected
 88 |     mock_api_module.get_task.assert_called_once_with("test-task-1")
 89 | 
 90 | 
 91 | @patch("mcp_server.api")
 92 | def test_add_task(mock_api_module, mock_api):
 93 |     """Test the add_task tool."""
 94 |     # Set the mock API
 95 |     mock_api_module.add_task.return_value = mock_api.add_task.return_value
 96 |     
 97 |     # Call the function
 98 |     result = mcp_server.add_task(
 99 |         title="New Task",
100 |         description="Description for new task",
101 |         priority=1,
102 |         status="not_started"
103 |     )
104 |     
105 |     # Assert the result
106 |     assert result == mock_api.add_task.return_value
107 |     mock_api_module.add_task.assert_called_once_with(
108 |         "New Task", "Description for new task", 1, "not_started"
109 |     )
110 |     mock_api_module.save_all.assert_called_once()
111 | 
112 | 
113 | def test_add_task_prompt():
114 |     """Test the add_task_prompt."""
115 |     result = mcp_server.add_task_prompt(
116 |         title="Test Task", 
117 |         description="Task description"
118 |     )
119 |     
120 |     assert "Test Task" in result
121 |     assert "Task description" in result
122 | 
123 | 
124 | def test_create_plan_prompt():
125 |     """Test the create_plan_prompt."""
126 |     result = mcp_server.create_plan_prompt()
127 |     
128 |     assert "project plan" in result.lower()
129 |     assert "clear steps" in result.lower()
```

--------------------------------------------------------------------------------
/app/core/task_manager.py:
--------------------------------------------------------------------------------

```python
  1 | import json
  2 | import os
  3 | import uuid
  4 | from datetime import datetime
  5 | 
  6 | 
  7 | class TaskManager:
  8 |     def __init__(self, file_path=None, notes_file_path=None):
  9 |         """Initialize the TaskManager with optional file paths."""
 10 |         home_dir = os.path.expanduser("~")
 11 |         task_dir = os.path.join(home_dir, ".tasktracker")
 12 |         os.makedirs(task_dir, exist_ok=True)
 13 |         
 14 |         if file_path is None:
 15 |             file_path = os.path.join(task_dir, "tasks.json")
 16 |         
 17 |         if notes_file_path is None:
 18 |             notes_file_path = os.path.join(task_dir, "notes.txt")
 19 |             
 20 |         self.file_path = file_path
 21 |         self.notes_file_path = notes_file_path
 22 |         self.tasks = self._load_tasks()
 23 |         self.notes = self._load_notes()
 24 |     
 25 |     def _load_tasks(self):
 26 |         """Load tasks from the file or return an empty list if file doesn't exist."""
 27 |         if os.path.exists(self.file_path):
 28 |             try:
 29 |                 with open(self.file_path, 'r') as f:
 30 |                     return json.load(f)
 31 |             except json.JSONDecodeError:
 32 |                 return []
 33 |         return []
 34 |         
 35 |     def reload_tasks(self):
 36 |         """Reload tasks from file (for external changes like MCP)."""
 37 |         self.tasks = self._load_tasks()
 38 |     
 39 |     def _load_notes(self):
 40 |         """Load notes from file or return empty string if file doesn't exist."""
 41 |         if os.path.exists(self.notes_file_path):
 42 |             try:
 43 |                 with open(self.notes_file_path, 'r') as f:
 44 |                     return f.read()
 45 |             except:
 46 |                 return ""
 47 |         return ""
 48 |         
 49 |     def reload_notes(self):
 50 |         """Reload notes from file (for external changes like MCP)."""
 51 |         self.notes = self._load_notes()
 52 |     
 53 |     def save_tasks(self):
 54 |         """Save tasks to the file."""
 55 |         with open(self.file_path, 'w') as f:
 56 |             json.dump(self.tasks, f, indent=2)
 57 |             
 58 |     def save_notes(self, notes_text):
 59 |         """Save notes to the file."""
 60 |         self.notes = notes_text
 61 |         with open(self.notes_file_path, 'w') as f:
 62 |             f.write(notes_text)
 63 |             
 64 |     def get_notes(self):
 65 |         """Get the current notes."""
 66 |         return self.notes
 67 |     
 68 |     def get_all_tasks(self):
 69 |         """Return all tasks."""
 70 |         return self.tasks
 71 |     
 72 |     def get_task(self, task_id):
 73 |         """Get a task by ID."""
 74 |         for task in self.tasks:
 75 |             if task["id"] == task_id:
 76 |                 return task
 77 |         return None
 78 |     
 79 |     def add_task(self, title, description="", priority=1, status="not_started"):
 80 |         """Add a new task."""
 81 |         # Validate status
 82 |         valid_statuses = ["not_started", "in_progress", "completed"]
 83 |         if status not in valid_statuses:
 84 |             status = "not_started"
 85 |             
 86 |         task = {
 87 |             "id": str(uuid.uuid4()),
 88 |             "title": title,
 89 |             "description": description,
 90 |             "priority": priority,
 91 |             "status": status,
 92 |             "created_at": datetime.now().isoformat(),
 93 |             "updated_at": datetime.now().isoformat()
 94 |         }
 95 |         self.tasks.append(task)
 96 |         self.save_tasks()
 97 |         return task
 98 |     
 99 |     def update_task(self, task_id, **kwargs):
100 |         """Update a task by ID."""
101 |         task = self.get_task(task_id)
102 |         if task:
103 |             for key, value in kwargs.items():
104 |                 if key in task and key not in ["id", "created_at"]:
105 |                     task[key] = value
106 |             task["updated_at"] = datetime.now().isoformat()
107 |             self.save_tasks()
108 |             return task
109 |         return None
110 |     
111 |     def delete_task(self, task_id):
112 |         """Delete a task by ID."""
113 |         task = self.get_task(task_id)
114 |         if task:
115 |             self.tasks.remove(task)
116 |             self.save_tasks()
117 |             return True
118 |         return False
```

--------------------------------------------------------------------------------
/app/api/api.py:
--------------------------------------------------------------------------------

```python
  1 | from app.core.task_manager import TaskManager
  2 | from app.core.plan_manager import PlanManager
  3 | 
  4 | 
  5 | class TaskTrackerAPI:
  6 |     def __init__(self, task_manager=None, plan_manager=None):
  7 |         """Initialize the TaskTrackerAPI with task and plan managers."""
  8 |         self.task_manager = task_manager or TaskManager()
  9 |         self.plan_manager = plan_manager or PlanManager()
 10 |     
 11 |     # Task methods
 12 |     def get_all_tasks(self):
 13 |         """Get all tasks."""
 14 |         return self.task_manager.get_all_tasks()
 15 |     
 16 |     def get_task(self, task_id):
 17 |         """Get a task by ID."""
 18 |         return self.task_manager.get_task(task_id)
 19 |     
 20 |     def add_task(self, title, description="", priority=1, status="pending"):
 21 |         """Add a new task."""
 22 |         return self.task_manager.add_task(title, description, priority, status)
 23 |     
 24 |     def update_task(self, task_id, **kwargs):
 25 |         """Update a task by ID."""
 26 |         return self.task_manager.update_task(task_id, **kwargs)
 27 |     
 28 |     def delete_task(self, task_id):
 29 |         """Delete a task by ID."""
 30 |         return self.task_manager.delete_task(task_id)
 31 |     
 32 |     # Plan methods
 33 |     def get_all_plan_steps(self):
 34 |         """Get all plan steps."""
 35 |         return self.plan_manager.get_all_steps()
 36 |     
 37 |     def get_plan_step(self, step_id):
 38 |         """Get a plan step by ID."""
 39 |         return self.plan_manager.get_step(step_id)
 40 |     
 41 |     def add_plan_step(self, name, description="", details="", order=None, completed=False):
 42 |         """Add a new plan step."""
 43 |         return self.plan_manager.add_step(name, description, details, order, completed)
 44 |     
 45 |     def update_plan_step(self, step_id, **kwargs):
 46 |         """Update a plan step by ID."""
 47 |         return self.plan_manager.update_step(step_id, **kwargs)
 48 |     
 49 |     def toggle_plan_step(self, step_id):
 50 |         """Toggle the completion status of a plan step."""
 51 |         return self.plan_manager.toggle_step(step_id)
 52 |     
 53 |     def delete_plan_step(self, step_id):
 54 |         """Delete a plan step by ID."""
 55 |         return self.plan_manager.delete_step(step_id)
 56 |     
 57 |     def reorder_plan_steps(self):
 58 |         """Reorder plan steps to ensure consistent ordering."""
 59 |         return self.plan_manager.reorder_steps()
 60 |     
 61 |     # Notes methods
 62 |     def get_notes(self):
 63 |         """Get the notes."""
 64 |         return self.task_manager.get_notes()
 65 |     
 66 |     def save_notes(self, notes_text):
 67 |         """Save notes."""
 68 |         self.task_manager.save_notes(notes_text)
 69 |         return True
 70 |     
 71 |     # Data management
 72 |     def save_all(self):
 73 |         """Save all data to files."""
 74 |         self.task_manager.save_tasks()
 75 |         self.plan_manager.save_plan()
 76 |         return True
 77 |         
 78 |     def reload_all(self):
 79 |         """Reload all data from files (for external changes like MCP)."""
 80 |         self.task_manager.reload_tasks()
 81 |         self.task_manager.reload_notes()
 82 |         self.plan_manager.reload_plan()
 83 |         return True
 84 |     
 85 |     def export_data(self, file_path):
 86 |         """Export all data to a single JSON file."""
 87 |         import json
 88 |         data = {
 89 |             "tasks": self.get_all_tasks(),
 90 |             "plan": self.get_all_plan_steps(),
 91 |             "notes": self.get_notes()
 92 |         }
 93 |         with open(file_path, 'w') as f:
 94 |             json.dump(data, f, indent=2)
 95 |         return True
 96 |     
 97 |     def import_data(self, file_path):
 98 |         """Import data from a JSON file."""
 99 |         import json
100 |         try:
101 |             with open(file_path, 'r') as f:
102 |                 data = json.load(f)
103 |             
104 |             # Clear existing data
105 |             self.task_manager.tasks = data.get("tasks", [])
106 |             self.plan_manager.plan_steps = data.get("plan", [])
107 |             
108 |             # Import notes if available
109 |             if "notes" in data:
110 |                 self.task_manager.save_notes(data["notes"])
111 |             
112 |             # Save imported data
113 |             self.save_all()
114 |             return True
115 |         except (json.JSONDecodeError, FileNotFoundError) as e:
116 |             print(f"Error importing data: {e}")
117 |             return False
```

--------------------------------------------------------------------------------
/MCP-README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # TaskTracker MCP Server
  2 | 
  3 | TaskTracker MCP is a Model Context Protocol compatible version of the Terminal Task Tracker application. It exposes task and project plan management capabilities through MCP, allowing Language Models to interact with your task tracking data.
  4 | 
  5 | ## Features
  6 | 
  7 | - **Resources**: Access tasks, plans, and notes data
  8 | - **Tools**: Create, update, and delete tasks and plan steps
  9 | - **Prompts**: Templates for common task management activities
 10 | 
 11 | ## Installation
 12 | 
 13 | ```bash
 14 | # Using uv (recommended)
 15 | uv add -e .
 16 | 
 17 | # Or with pip
 18 | pip install -e .
 19 | ```
 20 | 
 21 | ## Usage with Claude Desktop
 22 | 
 23 | ```bash
 24 | # Install in Claude Desktop
 25 | mcp install mcp_server_fixed.py
 26 | 
 27 | # Run with MCP Inspector
 28 | mcp dev mcp_server_fixed.py
 29 | ```
 30 | 
 31 | NOTE: If you encounter errors with `mcp_server.py`, please use the fixed version `mcp_server_fixed.py` instead. The fixed version uses a global API instance for resources and properly handles context parameters for tools.
 32 | 
 33 | ## Running Directly
 34 | 
 35 | ```bash
 36 | # Run server directly
 37 | python mcp_server_fixed.py
 38 | 
 39 | # Or using MCP CLI
 40 | mcp run mcp_server_fixed.py
 41 | ```
 42 | 
 43 | ## Resources
 44 | 
 45 | TaskTracker exposes the following resources:
 46 | 
 47 | - `tasks://all` - List all tasks
 48 | - `tasks://{task_id}` - Get a specific task
 49 | - `plan://all` - List all plan steps
 50 | - `plan://{step_id}` - Get a specific plan step
 51 | - `notes://all` - Get all notes
 52 | 
 53 | ## Tools
 54 | 
 55 | TaskTracker provides the following tools:
 56 | 
 57 | ### Task Tools
 58 | - `get_all_tasks_tool` - Get all tasks
 59 | - `get_task_tool` - Get a specific task by ID
 60 | - `add_task` - Create a new task
 61 | - `update_task` - Update an existing task
 62 | - `delete_task` - Delete a task
 63 | 
 64 | ### Plan Tools
 65 | - `get_all_plan_steps_tool` - Get all plan steps
 66 | - `get_plan_step_tool` - Get a specific plan step by ID
 67 | - `add_plan_step` - Create a new plan step
 68 | - `update_plan_step` - Update an existing plan step
 69 | - `delete_plan_step` - Delete a plan step
 70 | - `toggle_plan_step` - Toggle completion status
 71 | 
 72 | ### Notes and Data Tools
 73 | - `get_notes_tool` - Get all notes
 74 | - `save_notes` - Save notes
 75 | - `export_data` - Export all data to JSON
 76 | - `import_data` - Import data from JSON
 77 | 
 78 | ## Prompts
 79 | 
 80 | TaskTracker includes the following prompts:
 81 | 
 82 | - `add_task_prompt` - Template for adding a new task
 83 | - `create_plan_prompt` - Template for creating a project plan
 84 | 
 85 | ## Example Interactions
 86 | 
 87 | ### Adding and Retrieving Tasks
 88 | 
 89 | ```
 90 | > call-tool add_task "Implement login feature" "Create authentication endpoints for user login" 1 "not_started"
 91 | {
 92 |   "id": "550e8400-e29b-41d4-a716-446655440000",
 93 |   "title": "Implement login feature",
 94 |   "description": "Create authentication endpoints for user login",
 95 |   "priority": 1,
 96 |   "status": "not_started",
 97 |   "created_at": "2025-04-08T14:32:15.123456",
 98 |   "updated_at": "2025-04-08T14:32:15.123456"
 99 | }
100 | 
101 | > call-tool get_all_tasks_tool
102 | [
103 |   {
104 |     "id": "550e8400-e29b-41d4-a716-446655440000",
105 |     "title": "Implement login feature",
106 |     "description": "Create authentication endpoints for user login",
107 |     "priority": 1,
108 |     "status": "not_started",
109 |     "created_at": "2025-04-08T14:32:15.123456",
110 |     "updated_at": "2025-04-08T14:32:15.123456"
111 |   }
112 | ]
113 | 
114 | > read-resource tasks://all
115 | [
116 |   {
117 |     "id": "550e8400-e29b-41d4-a716-446655440000",
118 |     "title": "Implement login feature",
119 |     "description": "Create authentication endpoints for user login",
120 |     "priority": 1,
121 |     "status": "not_started",
122 |     "created_at": "2025-04-08T14:32:15.123456",
123 |     "updated_at": "2025-04-08T14:32:15.123456"
124 |   }
125 | ]
126 | ```
127 | 
128 | ### Managing Project Plans
129 | 
130 | ```
131 | > call-tool add_plan_step "Design API endpoints" "Create OpenAPI specification for endpoints" "Include authentication routes" 0 false
132 | {
133 |   "id": "550e8400-e29b-41d4-a716-446655440001",
134 |   "name": "Design API endpoints",
135 |   "description": "Create OpenAPI specification for endpoints",
136 |   "details": "Include authentication routes",
137 |   "order": 0,
138 |   "completed": false,
139 |   "created_at": "2025-04-08T14:33:15.123456",
140 |   "updated_at": "2025-04-08T14:33:15.123456"
141 | }
142 | 
143 | > call-tool get_all_plan_steps_tool
144 | [
145 |   {
146 |     "id": "550e8400-e29b-41d4-a716-446655440001",
147 |     "name": "Design API endpoints",
148 |     "description": "Create OpenAPI specification for endpoints",
149 |     "details": "Include authentication routes",
150 |     "order": 0,
151 |     "completed": false,
152 |     "created_at": "2025-04-08T14:33:15.123456",
153 |     "updated_at": "2025-04-08T14:33:15.123456"
154 |   }
155 | ]
156 | ```
157 | 
158 | ## License
159 | 
160 | MIT
```

--------------------------------------------------------------------------------
/app/core/plan_manager.py:
--------------------------------------------------------------------------------

```python
  1 | import json
  2 | import os
  3 | import uuid
  4 | from datetime import datetime
  5 | 
  6 | 
  7 | class PlanManager:
  8 |     def __init__(self, file_path=None):
  9 |         """Initialize the PlanManager with an optional file path."""
 10 |         if file_path is None:
 11 |             home_dir = os.path.expanduser("~")
 12 |             plan_dir = os.path.join(home_dir, ".tasktracker")
 13 |             os.makedirs(plan_dir, exist_ok=True)
 14 |             file_path = os.path.join(plan_dir, "plan.json")
 15 |         
 16 |         self.file_path = file_path
 17 |         self.plan_steps = self._load_plan()
 18 |     
 19 |     def _load_plan(self):
 20 |         """Load plan steps from the file or return an empty list if file doesn't exist."""
 21 |         if os.path.exists(self.file_path):
 22 |             try:
 23 |                 with open(self.file_path, 'r') as f:
 24 |                     data = json.load(f)
 25 |                     # Ensure we always return a list
 26 |                     if isinstance(data, list):
 27 |                         return data
 28 |                     elif isinstance(data, dict) and "steps" in data:
 29 |                         return data["steps"]
 30 |                     else:
 31 |                         # If it's not a list or doesn't have steps key, return empty list
 32 |                         return []
 33 |             except (json.JSONDecodeError, IOError):
 34 |                 return []
 35 |         return []
 36 |         
 37 |     def reload_plan(self):
 38 |         """Reload plan steps from file (for external changes like MCP)."""
 39 |         self.plan_steps = self._load_plan()
 40 |     
 41 |     def save_plan(self):
 42 |         """Save plan steps to the file."""
 43 |         # Ensure plan_steps is a list before saving
 44 |         if not isinstance(self.plan_steps, list):
 45 |             self.plan_steps = []
 46 |             
 47 |         with open(self.file_path, 'w') as f:
 48 |             json.dump(self.plan_steps, f, indent=2)
 49 |     
 50 |     def get_all_steps(self):
 51 |         """Return all plan steps."""
 52 |         # Ensure we always return a list
 53 |         if not isinstance(self.plan_steps, list):
 54 |             self.plan_steps = []
 55 |         return self.plan_steps
 56 |     
 57 |     def get_step(self, step_id):
 58 |         """Get a plan step by ID."""
 59 |         # Ensure plan_steps is a list
 60 |         if not isinstance(self.plan_steps, list):
 61 |             self.plan_steps = []
 62 |             return None
 63 |             
 64 |         for step in self.plan_steps:
 65 |             if step["id"] == step_id:
 66 |                 return step
 67 |         return None
 68 |     
 69 |     def add_step(self, name, description="", details="", order=None, completed=False):
 70 |         """Add a new plan step."""
 71 |         # Ensure plan_steps is a list
 72 |         if not isinstance(self.plan_steps, list):
 73 |             self.plan_steps = []
 74 |         
 75 |         if order is None:
 76 |             # Place at the end by default
 77 |             order = len(self.plan_steps)
 78 |         
 79 |         step = {
 80 |             "id": str(uuid.uuid4()),
 81 |             "name": name,
 82 |             "description": description,
 83 |             "details": details,
 84 |             "order": order,
 85 |             "completed": completed,
 86 |             "created_at": datetime.now().isoformat(),
 87 |             "updated_at": datetime.now().isoformat()
 88 |         }
 89 |         
 90 |         # Insert at the specified order
 91 |         self.plan_steps.append(step)
 92 |         self.reorder_steps()
 93 |         self.save_plan()
 94 |         return step
 95 |     
 96 |     def update_step(self, step_id, **kwargs):
 97 |         """Update a plan step by ID."""
 98 |         step = self.get_step(step_id)
 99 |         if step:
100 |             for key, value in kwargs.items():
101 |                 if key in step and key not in ["id", "created_at"]:
102 |                     step[key] = value
103 |             step["updated_at"] = datetime.now().isoformat()
104 |             
105 |             # If order changed, reorder all steps
106 |             if "order" in kwargs:
107 |                 self.reorder_steps()
108 |                 
109 |             self.save_plan()
110 |             return step
111 |         return None
112 |     
113 |     def toggle_step(self, step_id):
114 |         """Toggle the completion status of a plan step."""
115 |         step = self.get_step(step_id)
116 |         if step:
117 |             step["completed"] = not step["completed"]
118 |             step["updated_at"] = datetime.now().isoformat()
119 |             self.save_plan()
120 |             return step
121 |         return None
122 |     
123 |     def delete_step(self, step_id):
124 |         """Delete a plan step by ID."""
125 |         # Ensure plan_steps is a list
126 |         if not isinstance(self.plan_steps, list):
127 |             self.plan_steps = []
128 |             return False
129 |             
130 |         step = self.get_step(step_id)
131 |         if step:
132 |             self.plan_steps.remove(step)
133 |             self.reorder_steps()
134 |             self.save_plan()
135 |             return True
136 |         return False
137 |     
138 |     def reorder_steps(self):
139 |         """Reorder steps to ensure consistent ordering."""
140 |         # Ensure plan_steps is a list
141 |         if not isinstance(self.plan_steps, list):
142 |             self.plan_steps = []
143 |             return
144 |             
145 |         # Sort by the current order
146 |         self.plan_steps.sort(key=lambda x: x.get("order", 0))
147 |         
148 |         # Update order field to match actual position
149 |         for i, step in enumerate(self.plan_steps):
150 |             step["order"] = i
```

--------------------------------------------------------------------------------
/app/api/cli.py:
--------------------------------------------------------------------------------

```python
  1 | import argparse
  2 | import sys
  3 | import os
  4 | 
  5 | from app.api.api import TaskTrackerAPI
  6 | from app.core.task_manager import TaskManager
  7 | from app.core.plan_manager import PlanManager
  8 | 
  9 | 
 10 | def create_parser():
 11 |     """Create the command line argument parser."""
 12 |     parser = argparse.ArgumentParser(description='Terminal Task Tracker CLI')
 13 |     subparsers = parser.add_subparsers(dest='command', help='Command to execute')
 14 |     
 15 |     # Task commands
 16 |     task_parser = subparsers.add_parser('task', help='Task operations')
 17 |     task_subparsers = task_parser.add_subparsers(dest='subcommand', help='Task subcommand')
 18 |     
 19 |     # task list
 20 |     task_list_parser = task_subparsers.add_parser('list', help='List all tasks')
 21 |     
 22 |     # task show
 23 |     task_show_parser = task_subparsers.add_parser('show', help='Show task details')
 24 |     task_show_parser.add_argument('task_id', help='Task ID')
 25 |     
 26 |     # task add
 27 |     task_add_parser = task_subparsers.add_parser('add', help='Add a new task')
 28 |     task_add_parser.add_argument('title', help='Task title')
 29 |     task_add_parser.add_argument('--description', '-d', help='Task description')
 30 |     task_add_parser.add_argument('--priority', '-p', type=int, choices=[1, 2, 3], default=1, 
 31 |                                 help='Task priority (1=Low, 2=Medium, 3=High)')
 32 |     task_add_parser.add_argument('--status', '-s', 
 33 |                                 choices=['not_started', 'in_progress', 'completed'], 
 34 |                                 default='not_started', help='Task status')
 35 |     
 36 |     # task update
 37 |     task_update_parser = task_subparsers.add_parser('update', help='Update a task')
 38 |     task_update_parser.add_argument('task_id', help='Task ID')
 39 |     task_update_parser.add_argument('--title', '-t', help='Task title')
 40 |     task_update_parser.add_argument('--description', '-d', help='Task description')
 41 |     task_update_parser.add_argument('--priority', '-p', type=int, choices=[1, 2, 3], 
 42 |                                    help='Task priority (1=Low, 2=Medium, 3=High)')
 43 |     task_update_parser.add_argument('--status', '-s', 
 44 |                                    choices=['not_started', 'in_progress', 'completed'], 
 45 |                                    help='Task status')
 46 |     
 47 |     # task delete
 48 |     task_delete_parser = task_subparsers.add_parser('delete', help='Delete a task')
 49 |     task_delete_parser.add_argument('task_id', help='Task ID')
 50 |     
 51 |     # Plan commands
 52 |     plan_parser = subparsers.add_parser('plan', help='Plan operations')
 53 |     plan_subparsers = plan_parser.add_subparsers(dest='subcommand', help='Plan subcommand')
 54 |     
 55 |     # plan list
 56 |     plan_list_parser = plan_subparsers.add_parser('list', help='List all plan steps')
 57 |     
 58 |     # plan show
 59 |     plan_show_parser = plan_subparsers.add_parser('show', help='Show plan step details')
 60 |     plan_show_parser.add_argument('step_id', help='Step ID')
 61 |     
 62 |     # plan add
 63 |     plan_add_parser = plan_subparsers.add_parser('add', help='Add a new plan step')
 64 |     plan_add_parser.add_argument('name', help='Step name')
 65 |     plan_add_parser.add_argument('--description', '-d', help='Brief description')
 66 |     plan_add_parser.add_argument('--details', '-D', help='Detailed information')
 67 |     plan_add_parser.add_argument('--order', '-o', type=int, help='Step order (position in plan)')
 68 |     plan_add_parser.add_argument('--completed', '-c', action='store_true', 
 69 |                                 help='Mark step as completed')
 70 |     
 71 |     # plan update
 72 |     plan_update_parser = plan_subparsers.add_parser('update', help='Update a plan step')
 73 |     plan_update_parser.add_argument('step_id', help='Step ID')
 74 |     plan_update_parser.add_argument('--name', '-n', help='Step name')
 75 |     plan_update_parser.add_argument('--description', '-d', help='Brief description')
 76 |     plan_update_parser.add_argument('--details', '-D', help='Detailed information')
 77 |     plan_update_parser.add_argument('--order', '-o', type=int, help='Step order (position in plan)')
 78 |     
 79 |     # plan toggle
 80 |     plan_toggle_parser = plan_subparsers.add_parser('toggle', 
 81 |                                                    help='Toggle completion status of a plan step')
 82 |     plan_toggle_parser.add_argument('step_id', help='Step ID')
 83 |     
 84 |     # plan delete
 85 |     plan_delete_parser = plan_subparsers.add_parser('delete', help='Delete a plan step')
 86 |     plan_delete_parser.add_argument('step_id', help='Step ID')
 87 |     
 88 |     # Export/Import commands
 89 |     export_parser = subparsers.add_parser('export', help='Export data to JSON file')
 90 |     export_parser.add_argument('file_path', help='Path to export file')
 91 |     
 92 |     import_parser = subparsers.add_parser('import', help='Import data from JSON file')
 93 |     import_parser.add_argument('file_path', help='Path to import file')
 94 |     
 95 |     return parser
 96 | 
 97 | 
 98 | def main():
 99 |     """Main CLI entry point."""
100 |     parser = create_parser()
101 |     args = parser.parse_args()
102 |     
103 |     # Initialize API
104 |     api = TaskTrackerAPI()
105 |     
106 |     if not args.command:
107 |         parser.print_help()
108 |         return
109 |     
110 |     try:
111 |         # Task commands
112 |         if args.command == 'task':
113 |             if args.subcommand == 'list':
114 |                 tasks = api.get_all_tasks()
115 |                 if not tasks:
116 |                     print("No tasks found.")
117 |                 else:
118 |                     print(f"{'ID':<36} {'Title':<30} {'Priority':<8} {'Status':<12}")
119 |                     print("-" * 90)
120 |                     for task in tasks:
121 |                         print(f"{task['id']:<36} {task['title'][:30]:<30} {task['priority']:<8} {task['status']:<12}")
122 |             
123 |             elif args.subcommand == 'show':
124 |                 task = api.get_task(args.task_id)
125 |                 if task:
126 |                     print(f"ID: {task['id']}")
127 |                     print(f"Title: {task['title']}")
128 |                     print(f"Description: {task['description']}")
129 |                     print(f"Priority: {task['priority']}")
130 |                     print(f"Status: {task['status']}")
131 |                     print(f"Created: {task['created_at']}")
132 |                     print(f"Updated: {task['updated_at']}")
133 |                 else:
134 |                     print(f"Task not found: {args.task_id}")
135 |             
136 |             elif args.subcommand == 'add':
137 |                 task = api.add_task(
138 |                     args.title,
139 |                     args.description or "",
140 |                     args.priority,
141 |                     args.status
142 |                 )
143 |                 print(f"Task added: {task['id']}")
144 |             
145 |             elif args.subcommand == 'update':
146 |                 # Collect the fields to update
147 |                 update_fields = {}
148 |                 if args.title:
149 |                     update_fields['title'] = args.title
150 |                 if args.description:
151 |                     update_fields['description'] = args.description
152 |                 if args.priority:
153 |                     update_fields['priority'] = args.priority
154 |                 if args.status:
155 |                     update_fields['status'] = args.status
156 |                 
157 |                 task = api.update_task(args.task_id, **update_fields)
158 |                 if task:
159 |                     print(f"Task updated: {task['id']}")
160 |                 else:
161 |                     print(f"Task not found: {args.task_id}")
162 |             
163 |             elif args.subcommand == 'delete':
164 |                 result = api.delete_task(args.task_id)
165 |                 if result:
166 |                     print(f"Task deleted: {args.task_id}")
167 |                 else:
168 |                     print(f"Task not found: {args.task_id}")
169 |             
170 |             else:
171 |                 parser.print_help()
172 |         
173 |         # Plan commands
174 |         elif args.command == 'plan':
175 |             if args.subcommand == 'list':
176 |                 steps = api.get_all_plan_steps()
177 |                 if not steps:
178 |                     print("No plan steps found.")
179 |                 else:
180 |                     print(f"{'Order':<6} {'Completed':<10} {'ID':<36} {'Description'}")
181 |                     print("-" * 90)
182 |                     for step in steps:
183 |                         completed = "[x]" if step['completed'] else "[ ]"
184 |                         print(f"{step['order']:<6} {completed:<10} {step['id']:<36} {step['description']}")
185 |             
186 |             elif args.subcommand == 'show':
187 |                 step = api.get_plan_step(args.step_id)
188 |                 if step:
189 |                     completed = "Yes" if step['completed'] else "No"
190 |                     print(f"ID: {step['id']}")
191 |                     print(f"Name: {step.get('name', 'N/A')}")
192 |                     print(f"Description: {step.get('description', '')}")
193 |                     print(f"Order: {step.get('order', 0)}")
194 |                     print(f"Completed: {completed}")
195 |                     
196 |                     # Print details if available
197 |                     details = step.get('details', '')
198 |                     if details:
199 |                         print("\nDetails:")
200 |                         print(details)
201 |                         
202 |                     print(f"\nCreated: {step.get('created_at', 'N/A')}")
203 |                     print(f"Updated: {step.get('updated_at', 'N/A')}")
204 |                 else:
205 |                     print(f"Plan step not found: {args.step_id}")
206 |             
207 |             elif args.subcommand == 'add':
208 |                 step = api.add_plan_step(
209 |                     args.name,
210 |                     args.description or "",
211 |                     args.details or "",
212 |                     args.order,
213 |                     args.completed
214 |                 )
215 |                 print(f"Plan step added: {step['id']}")
216 |             
217 |             elif args.subcommand == 'update':
218 |                 # Collect the fields to update
219 |                 update_fields = {}
220 |                 if args.name:
221 |                     update_fields['name'] = args.name
222 |                 if args.description:
223 |                     update_fields['description'] = args.description
224 |                 if args.details:
225 |                     update_fields['details'] = args.details
226 |                 if args.order is not None:
227 |                     update_fields['order'] = args.order
228 |                 
229 |                 step = api.update_plan_step(args.step_id, **update_fields)
230 |                 if step:
231 |                     print(f"Plan step updated: {step['id']}")
232 |                 else:
233 |                     print(f"Plan step not found: {args.step_id}")
234 |             
235 |             elif args.subcommand == 'toggle':
236 |                 step = api.toggle_plan_step(args.step_id)
237 |                 if step:
238 |                     completed = "completed" if step['completed'] else "not completed"
239 |                     print(f"Plan step {args.step_id} marked as {completed}")
240 |                 else:
241 |                     print(f"Plan step not found: {args.step_id}")
242 |             
243 |             elif args.subcommand == 'delete':
244 |                 result = api.delete_plan_step(args.step_id)
245 |                 if result:
246 |                     print(f"Plan step deleted: {args.step_id}")
247 |                 else:
248 |                     print(f"Plan step not found: {args.step_id}")
249 |             
250 |             else:
251 |                 parser.print_help()
252 |         
253 |         # Export/Import commands
254 |         elif args.command == 'export':
255 |             result = api.export_data(args.file_path)
256 |             if result:
257 |                 print(f"Data exported to {args.file_path}")
258 |             else:
259 |                 print("Error exporting data")
260 |         
261 |         elif args.command == 'import':
262 |             result = api.import_data(args.file_path)
263 |             if result:
264 |                 print(f"Data imported from {args.file_path}")
265 |             else:
266 |                 print("Error importing data")
267 |         
268 |         else:
269 |             parser.print_help()
270 |     
271 |     except Exception as e:
272 |         print(f"Error: {str(e)}")
273 |         return 1
274 |     
275 |     return 0
276 | 
277 | 
278 | if __name__ == "__main__":
279 |     sys.exit(main())
```

--------------------------------------------------------------------------------
/app/ui/terminal_ui.py:
--------------------------------------------------------------------------------

```python
  1 | import curses
  2 | import traceback
  3 | import os
  4 | import time
  5 | 
  6 | from app.ui.ui_components import TaskListWindow, TaskDetailWindow, PlanWindow, NotesWindow, InputDialog, ConfirmDialog
  7 | from app.ui.input_handler import InputHandler, FocusArea
  8 | 
  9 | 
 10 | class TerminalUI:
 11 |     def __init__(self, api):
 12 |         """Initialize the terminal UI with a reference to the API."""
 13 |         self.api = api
 14 |         self.stdscr = None
 15 |         self.task_list_win = None
 16 |         self.task_detail_win = None
 17 |         self.plan_win = None
 18 |         self.notes_win = None
 19 |         self.input_handler = None
 20 |         self.notes_visible = True  # Flag to control notes visibility
 21 |         
 22 |         # File modification tracking
 23 |         self.last_tasks_mtime = 0
 24 |         self.last_plan_mtime = 0
 25 |         self.last_notes_mtime = 0
 26 |         self.last_check_time = 0
 27 |         self.file_check_interval = 1.0  # Check for file changes every second
 28 |     
 29 |     def run(self):
 30 |         """Run the terminal UI."""
 31 |         try:
 32 |             # Start curses application
 33 |             curses.wrapper(self._main)
 34 |         except Exception as e:
 35 |             # If an error occurs, restore terminal and show traceback
 36 |             if self.stdscr:
 37 |                 # Reset timeout to blocking before exiting to prevent potential issues
 38 |                 self.stdscr.timeout(-1)
 39 |                 curses.endwin()
 40 |             print(f"An error occurred: {str(e)}")
 41 |             traceback.print_exc()
 42 |     
 43 |     def _main(self, stdscr):
 44 |         """Main function for the curses application."""
 45 |         self.stdscr = stdscr
 46 |         
 47 |         # Set up curses
 48 |         curses.curs_set(0)  # Hide cursor
 49 |         stdscr.clear()
 50 |         
 51 |         # Set up input handler
 52 |         self.input_handler = InputHandler(self)
 53 |         
 54 |         # Create initial layout
 55 |         self._create_layout()
 56 |         
 57 |         # Initial data load
 58 |         try:
 59 |             self.refresh_tasks()
 60 |             self.refresh_plan()
 61 |             self.refresh_notes()
 62 |             
 63 |             # Initialize last modified times after initial load
 64 |             task_file = self.api.task_manager.file_path
 65 |             plan_file = self.api.plan_manager.file_path
 66 |             notes_file = self.api.task_manager.notes_file_path
 67 |             
 68 |             if os.path.exists(task_file):
 69 |                 self.last_tasks_mtime = os.path.getmtime(task_file)
 70 |             if os.path.exists(plan_file):
 71 |                 self.last_plan_mtime = os.path.getmtime(plan_file)
 72 |             if os.path.exists(notes_file):
 73 |                 self.last_notes_mtime = os.path.getmtime(notes_file)
 74 |                 
 75 |             self.last_check_time = time.time()
 76 |             
 77 |         except Exception as e:
 78 |             self.show_message(f"Error loading data: {str(e)}")
 79 |         
 80 |         # Main event loop
 81 |         while True:
 82 |             # Check for external file changes (e.g., from MCP)
 83 |             self.check_file_changes()
 84 |             
 85 |             # Update the screen
 86 |             stdscr.refresh()
 87 |             
 88 |             # Configure timeout for getch to allow polling for file changes
 89 |             stdscr.timeout(100)  # 100ms timeout
 90 |             
 91 |             # Get input (returns -1 if no input available)
 92 |             key = stdscr.getch()
 93 |             
 94 |             # Reset timeout to blocking mode if we actually got a key
 95 |             if key != -1:
 96 |                 stdscr.timeout(-1)
 97 |                 # Handle input (exit if handler returns False)
 98 |                 if not self.input_handler.handle_input(key):
 99 |                     break
100 |             else:
101 |                 # No input, just continue the loop to check for file changes
102 |                 continue
103 |     
104 |     def _create_layout(self):
105 |         """Create the initial window layout."""
106 |         screen_height, screen_width = self.stdscr.getmaxyx()
107 |         
108 |         # Calculate dimensions for initial layout
109 |         top_height = screen_height // 2
110 |         main_width = screen_width - 30  # Reserve 30 cols for notes on the right
111 |         task_width = main_width // 2
112 |         detail_width = main_width - task_width
113 |         plan_height = screen_height - top_height
114 |         notes_width = screen_width - main_width
115 |         
116 |         # Create windows
117 |         self.task_list_win = TaskListWindow(
118 |             self.stdscr, top_height, task_width, 0, 0, "Tasks"
119 |         )
120 |         
121 |         self.task_detail_win = TaskDetailWindow(
122 |             self.stdscr, top_height, detail_width, 0, task_width, "Task Details"
123 |         )
124 |         
125 |         self.plan_win = PlanWindow(
126 |             self.stdscr, plan_height, main_width, top_height, 0, "Project Plan"
127 |         )
128 |         
129 |         self.notes_win = NotesWindow(
130 |             self.stdscr, screen_height, notes_width, 0, main_width, "Notes"
131 |         )
132 |         
133 |         # Initial refresh
134 |         self.task_list_win.refresh()
135 |         self.task_detail_win.refresh()
136 |         self.plan_win.refresh()
137 |         
138 |         if self.notes_visible:
139 |             self.notes_win.refresh()
140 |     
141 |     def toggle_notes_visibility(self):
142 |         """Toggle the visibility of the notes window."""
143 |         self.notes_visible = not self.notes_visible
144 |         
145 |         # If hiding and notes is the active focus, change focus to tasks
146 |         if not self.notes_visible and self.input_handler.focus == FocusArea.NOTES:
147 |             self.input_handler.focus = FocusArea.TASKS
148 |             self.update_focus(FocusArea.TASKS)
149 |         
150 |         # Redraw layout (this will resize all windows accordingly)
151 |         self._resize_layout()
152 |         
153 |         # If notes are hidden, make sure we redraw all other windows
154 |         if not self.notes_visible:
155 |             # Ensure each window is refreshed with its contents
156 |             self.task_list_win.refresh_content()
157 |             self.task_detail_win.refresh_content()
158 |             self.plan_win.refresh_content()
159 |             
160 |             # Refresh the stdscr to ensure proper redraw of everything
161 |             self.stdscr.refresh()
162 |             
163 |         return self.notes_visible
164 |     
165 |     def _resize_layout(self):
166 |         """Resize the window layout."""
167 |         screen_height, screen_width = self.stdscr.getmaxyx()
168 |         
169 |         # Calculate dimensions based on notes visibility
170 |         if self.notes_visible:
171 |             main_width = screen_width - 30  # Reserve 30 cols for notes on the right
172 |             notes_width = screen_width - main_width
173 |         else:
174 |             main_width = screen_width  # Use full width when notes are hidden
175 |             notes_width = 0
176 |             
177 |         top_height = screen_height // 2
178 |         task_width = main_width // 2
179 |         detail_width = main_width - task_width
180 |         plan_height = screen_height - top_height
181 |         
182 |         # Resize windows
183 |         self.task_list_win.resize(top_height, task_width, 0, 0)
184 |         self.task_detail_win.resize(top_height, detail_width, 0, task_width)
185 |         self.plan_win.resize(plan_height, main_width, top_height, 0)
186 |         
187 |         # Only resize notes window if visible
188 |         if self.notes_visible:
189 |             self.notes_win.resize(screen_height, notes_width, 0, main_width)
190 |         
191 |         # Refresh content
192 |         self.task_list_win.refresh_content()
193 |         self.task_detail_win.refresh_content()
194 |         self.plan_win.refresh_content()
195 |         
196 |         # Only refresh notes if visible
197 |         if self.notes_visible:
198 |             self.notes_win.refresh_content()
199 |     
200 |     def refresh_tasks(self):
201 |         """Refresh task list and details."""
202 |         tasks = self.api.get_all_tasks()
203 |         
204 |         # Sort tasks by priority (high to low) and then by status
205 |         tasks.sort(key=lambda x: (-x['priority'], x['status']))
206 |         
207 |         self.task_list_win.set_tasks(tasks)
208 |         
209 |         # Update task details if there's a selected task
210 |         selected_task = self.task_list_win.get_selected_task()
211 |         self.task_detail_win.set_task(selected_task)
212 |     
213 |     def refresh_plan(self):
214 |         """Refresh the project plan."""
215 |         try:
216 |             steps = self.api.get_all_plan_steps()
217 |             
218 |             # Validate steps before setting them
219 |             if steps is None:
220 |                 steps = []
221 |                 
222 |             # Steps are already sorted by order in the API
223 |             self.plan_win.set_steps(steps)
224 |         except Exception as e:
225 |             # Handle errors gracefully
226 |             self.plan_win.set_steps([])
227 |             raise Exception(f"Failed to load plan: {str(e)}")
228 |     
229 |     def refresh_notes(self):
230 |         """Load and refresh the notes content."""
231 |         notes = self.api.get_notes()
232 |         self.notes_win.set_notes(notes)
233 |     
234 |     def save_notes(self):
235 |         """Save the current notes content."""
236 |         notes_text = self.notes_win.get_notes()
237 |         self.api.save_notes(notes_text)
238 |         
239 |     def check_file_changes(self):
240 |         """Check if any data files have been modified externally (like by MCP)."""
241 |         try:
242 |             current_time = time.time()
243 |             
244 |             # Only check periodically to reduce file system access
245 |             if current_time - self.last_check_time < self.file_check_interval:
246 |                 return False
247 |                 
248 |             self.last_check_time = current_time
249 |             changes_detected = False
250 |             
251 |             # Get file paths from the API's managers
252 |             task_file = self.api.task_manager.file_path
253 |             plan_file = self.api.plan_manager.file_path
254 |             notes_file = self.api.task_manager.notes_file_path
255 |             
256 |             # Check if any data files have been modified
257 |             tasks_changed = os.path.exists(task_file) and os.path.getmtime(task_file) > self.last_tasks_mtime
258 |             plan_changed = os.path.exists(plan_file) and os.path.getmtime(plan_file) > self.last_plan_mtime
259 |             notes_changed = os.path.exists(notes_file) and os.path.getmtime(notes_file) > self.last_notes_mtime
260 |             
261 |             if tasks_changed or plan_changed or notes_changed:
262 |                 # Update last modified times
263 |                 if tasks_changed:
264 |                     self.last_tasks_mtime = os.path.getmtime(task_file)
265 |                 if plan_changed:
266 |                     self.last_plan_mtime = os.path.getmtime(plan_file)
267 |                 if notes_changed:
268 |                     self.last_notes_mtime = os.path.getmtime(notes_file)
269 |                 
270 |                 try:
271 |                     # Reload all data from files
272 |                     self.api.reload_all()
273 |                     
274 |                     # Refresh UI components with individual try-except blocks
275 |                     try:
276 |                         if tasks_changed:
277 |                             self.refresh_tasks()
278 |                     except Exception as e:
279 |                         # Silently handle task refresh error
280 |                         pass
281 |                         
282 |                     try:
283 |                         if plan_changed:
284 |                             self.refresh_plan()
285 |                     except Exception as e:
286 |                         # Silently handle plan refresh error
287 |                         pass
288 |                         
289 |                     try:
290 |                         if notes_changed:
291 |                             self.refresh_notes()
292 |                     except Exception as e:
293 |                         # Silently handle notes refresh error
294 |                         pass
295 |                     
296 |                     changes_detected = True
297 |                 except Exception as e:
298 |                     # If reload fails, try to continue without crashing
299 |                     pass
300 |                     
301 |             return changes_detected
302 |         except Exception as e:
303 |             # Fail silently if file checking itself fails
304 |             return False
305 |     
306 |     def update_focus(self, focus):
307 |         """Update the UI focus."""
308 |         # Reset all titles first
309 |         self.task_list_win.set_title("Tasks")
310 |         self.task_detail_win.set_title("Task Details")
311 |         self.plan_win.set_title("Project Plan")
312 |         if self.notes_visible:
313 |             self.notes_win.set_title("Notes")
314 |         
315 |         # Highlight the active window by changing its title
316 |         if focus == FocusArea.TASKS:
317 |             self.task_list_win.set_title("Tasks [Active]")
318 |         elif focus == FocusArea.DETAILS:
319 |             self.task_detail_win.set_title("Task Details [Active]")
320 |         elif focus == FocusArea.PLAN:
321 |             self.plan_win.set_title("Project Plan [Active]")
322 |         elif focus == FocusArea.NOTES and self.notes_visible:
323 |             self.notes_win.set_title("Notes [Active]")
324 |         
325 |         # Clear screen to remove artifacts
326 |         self.stdscr.erase()
327 |         self.stdscr.refresh()
328 |         
329 |         # Refresh the content of all windows
330 |         self.task_list_win.refresh_content()
331 |         self.task_detail_win.refresh_content()
332 |         self.plan_win.refresh_content()
333 |         
334 |         # Only refresh notes if visible
335 |         if self.notes_visible:
336 |             self.notes_win.refresh_content()
337 |     
338 |     def show_input_dialog(self, title, prompts, initial_values=None):
339 |         """Show an input dialog and return the entered values or None if canceled."""
340 |         dialog = InputDialog(self.stdscr, title, prompts, initial_values)
341 |         result = dialog.show()
342 |         
343 |         # Redraw the entire screen after dialog closes
344 |         self.stdscr.clear()
345 |         self.stdscr.refresh()
346 |         self._resize_layout()
347 |         
348 |         return result
349 |     
350 |     def show_confirm_dialog(self, title, message):
351 |         """Show a confirmation dialog and return True if confirmed, False otherwise."""
352 |         dialog = ConfirmDialog(self.stdscr, title, message)
353 |         result = dialog.show()
354 |         
355 |         # Redraw the entire screen after dialog closes
356 |         self.stdscr.clear()
357 |         self.stdscr.refresh()
358 |         self._resize_layout()
359 |         
360 |         return result
361 |     
362 |     def show_message(self, message):
363 |         """Show a temporary message at the bottom of the screen."""
364 |         screen_height, screen_width = self.stdscr.getmaxyx()
365 |         
366 |         # Create a small window for the message
367 |         msg_height = 3
368 |         msg_width = min(len(message) + 4, screen_width - 4)
369 |         msg_y = (screen_height - msg_height) // 2
370 |         msg_x = (screen_width - msg_width) // 2
371 |         
372 |         # Create message window
373 |         msg_win = self.stdscr.subwin(msg_height, msg_width, msg_y, msg_x)
374 |         msg_win.box()
375 |         msg_win.addstr(1, 2, message[:msg_width - 4])
376 |         msg_win.addstr(msg_height - 1, 2, "Press any key to continue")
377 |         msg_win.refresh()
378 |         
379 |         # Wait for a key press
380 |         self.stdscr.getch()
381 |         
382 |         # Redraw the entire screen
383 |         self.stdscr.clear()
384 |         self.stdscr.refresh()
385 |         self._resize_layout()
```

--------------------------------------------------------------------------------
/app/ui/input_handler.py:
--------------------------------------------------------------------------------

```python
  1 | import curses
  2 | from enum import Enum
  3 | 
  4 | class FocusArea(Enum):
  5 |     TASKS = 0
  6 |     DETAILS = 1
  7 |     PLAN = 2
  8 |     NOTES = 3
  9 | 
 10 | 
 11 | class InputHandler:
 12 |     def __init__(self, terminal_ui):
 13 |         """Initialize the input handler with a reference to the terminal UI."""
 14 |         self.terminal_ui = terminal_ui
 15 |         self.focus = FocusArea.TASKS
 16 |     
 17 |     def handle_input(self, key):
 18 |         """
 19 |         Handle keyboard input and dispatch to appropriate handlers.
 20 |         Returns True if the application should continue, False if it should exit.
 21 |         """
 22 |         # If notes is in edit mode, we need special handling
 23 |         if self.focus == FocusArea.NOTES and self.terminal_ui.notes_win.edit_mode:
 24 |             # Escape exits edit mode in notes
 25 |             if key == 27:  # Escape
 26 |                 self.terminal_ui.notes_win.toggle_edit_mode()
 27 |                 self.terminal_ui.save_notes()
 28 |                 return True
 29 |             
 30 |             # All other keys are processed by the notes window
 31 |             try:
 32 |                 self.terminal_ui.notes_win.handle_key(key)
 33 |                 return True
 34 |             except Exception as e:
 35 |                 self.terminal_ui.show_message(f"Error in notes edit: {str(e)}")
 36 |                 return True
 37 |         
 38 |         # Global keys (work in any context)
 39 |         if key == 27:  # Escape
 40 |             return self._handle_escape()
 41 |         elif key == 9:  # Tab
 42 |             self._cycle_focus()
 43 |             return True
 44 |         elif key == 24:  # Ctrl+X - toggle notes visibility (ASCII 24 is Ctrl+X)
 45 |             self.terminal_ui.toggle_notes_visibility()
 46 |             return True
 47 |         
 48 |         # Focus-specific input handling
 49 |         if self.focus == FocusArea.TASKS:
 50 |             return self._handle_tasks_input(key)
 51 |         elif self.focus == FocusArea.DETAILS:
 52 |             return self._handle_details_input(key)
 53 |         elif self.focus == FocusArea.PLAN:
 54 |             return self._handle_plan_input(key)
 55 |         elif self.focus == FocusArea.NOTES:
 56 |             return self._handle_notes_input(key)
 57 |         
 58 |         return True
 59 |     
 60 |     def _handle_escape(self):
 61 |         """Handle the escape key - confirm exit."""
 62 |         # Reset timeout to blocking for the confirmation dialog
 63 |         self.terminal_ui.stdscr.timeout(-1)
 64 |         
 65 |         confirm = self.terminal_ui.show_confirm_dialog(
 66 |             "Exit Confirmation", 
 67 |             "Are you sure you want to exit? Any unsaved changes will be lost."
 68 |         )
 69 |         return not confirm  # Return False to exit if confirmed
 70 |     
 71 |     def _cycle_focus(self):
 72 |         """Cycle through the focus areas."""
 73 |         focus_order = list(FocusArea)
 74 |         current_idx = focus_order.index(self.focus)
 75 |         
 76 |         # Skip Notes focus if it's not visible
 77 |         if not self.terminal_ui.notes_visible:
 78 |             # Create a filtered list without the NOTES enum
 79 |             focus_order = [f for f in focus_order if f != FocusArea.NOTES]
 80 |         
 81 |         # Find the next focus in our (potentially filtered) list
 82 |         next_idx = (focus_order.index(self.focus) + 1) % len(focus_order)
 83 |         self.focus = focus_order[next_idx]
 84 |         
 85 |         # Update UI with new focus
 86 |         self.terminal_ui.update_focus(self.focus)
 87 |         
 88 |         # Force a complete UI redraw to fix rendering artifacts
 89 |         self.terminal_ui._resize_layout()
 90 |     
 91 |     def _handle_tasks_input(self, key):
 92 |         """Handle input while focused on the task list."""
 93 |         if key == curses.KEY_UP:
 94 |             self.terminal_ui.task_list_win.select_prev()
 95 |             self._update_task_details()
 96 |         
 97 |         elif key == curses.KEY_DOWN:
 98 |             self.terminal_ui.task_list_win.select_next()
 99 |             self._update_task_details()
100 |         
101 |         elif key in (10, 13, curses.KEY_ENTER):  # Enter (different codes)
102 |             # Toggle completion status when Enter is pressed
103 |             self._toggle_selected_task()
104 |             self._update_task_details()
105 |         
106 |         elif key == ord(' '):  # Space 
107 |             # Toggle completion status when Space is pressed
108 |             self._toggle_selected_task()
109 |             self._update_task_details()
110 |         
111 |         elif key == ord('n'):  # New task
112 |             self._new_task()
113 |         
114 |         elif key == ord('e'):  # Edit task
115 |             self._edit_task()
116 |         
117 |         elif key == ord('d'):  # Delete task
118 |             self._delete_task()
119 |         
120 |         return True
121 |     
122 |     def _handle_details_input(self, key):
123 |         """Handle input while focused on the task details."""
124 |         # There's not much to do in the details view except view
125 |         # Maybe implement scrolling for long descriptions later
126 |         return True
127 |         
128 |     def _handle_notes_input(self, key):
129 |         """Handle input while focused on the notes."""
130 |         if key == ord('e'):  # Edit notes
131 |             self.terminal_ui.notes_win.toggle_edit_mode()
132 |             return True
133 |         
134 |         return True
135 |     
136 |     def _handle_plan_input(self, key):
137 |         """Handle input while focused on the project plan."""
138 |         if key == curses.KEY_UP:
139 |             self.terminal_ui.plan_win.select_prev()
140 |         
141 |         elif key == curses.KEY_DOWN:
142 |             self.terminal_ui.plan_win.select_next()
143 |         
144 |         elif key in (10, 13, curses.KEY_ENTER):  # Enter (different codes)
145 |             # Toggle completion when Enter is pressed
146 |             self._toggle_plan_step()
147 |         
148 |         elif key == ord(' '):  # Toggle completion with Space
149 |             self._toggle_plan_step()
150 |         
151 |         elif key == ord('d'):  # Toggle details view
152 |             self.terminal_ui.plan_win.toggle_details()
153 |         
154 |         elif key == ord('n'):  # New plan step
155 |             self._new_plan_step()
156 |         
157 |         elif key == ord('e'):  # Edit plan step
158 |             self._edit_plan_step()
159 |         
160 |         elif key == ord('D'):  # Delete plan step (capital D to avoid conflict with details)
161 |             self._delete_plan_step()
162 |         
163 |         return True
164 |     
165 |     def _update_task_details(self):
166 |         """Update the task details window with the selected task."""
167 |         task = self.terminal_ui.task_list_win.get_selected_task()
168 |         self.terminal_ui.task_detail_win.set_task(task)
169 |     
170 |     def _new_task(self):
171 |         """Create a new task."""
172 |         prompts = ["Title", "Description", "Priority (1-3)"]
173 |         values = self.terminal_ui.show_input_dialog("New Task", prompts)
174 |         
175 |         if values:
176 |             title, description, priority_str = values
177 |             
178 |             # Validate priority
179 |             try:
180 |                 priority = int(priority_str) if priority_str else 1
181 |                 if priority < 1 or priority > 3:
182 |                     priority = 1
183 |             except ValueError:
184 |                 priority = 1
185 |             
186 |             # Add the task
187 |             task = self.terminal_ui.api.add_task(title, description, priority)
188 |             
189 |             # Refresh task list
190 |             self.terminal_ui.refresh_tasks()
191 |             
192 |             # Find and select the new task
193 |             tasks = self.terminal_ui.api.get_all_tasks()
194 |             for i, t in enumerate(tasks):
195 |                 if t["id"] == task["id"]:
196 |                     self.terminal_ui.task_list_win.selected_index = i
197 |                     self.terminal_ui.task_list_win.adjust_selection()
198 |                     self.terminal_ui.task_list_win.refresh_content()
199 |                     self._update_task_details()
200 |                     break
201 |     
202 |     def _edit_task(self):
203 |         """Edit the selected task."""
204 |         task = self.terminal_ui.task_list_win.get_selected_task()
205 |         if not task:
206 |             return
207 |         
208 |         # Set up the edit dialog with current values
209 |         prompts = ["Title", "Description", "Priority (1-3)", "Status"]
210 |         values = [
211 |             task["title"],
212 |             task["description"],
213 |             str(task["priority"]),
214 |             task["status"]
215 |         ]
216 |         
217 |         new_values = self.terminal_ui.show_input_dialog("Edit Task", prompts, values)
218 |         
219 |         if new_values:
220 |             title, description, priority_str, status = new_values
221 |             
222 |             # Validate priority
223 |             try:
224 |                 priority = int(priority_str) if priority_str else task["priority"]
225 |                 if priority < 1 or priority > 3:
226 |                     priority = task["priority"]
227 |             except ValueError:
228 |                 priority = task["priority"]
229 |             
230 |             # Validate status
231 |             valid_statuses = ["not_started", "in_progress", "completed"]
232 |             if status not in valid_statuses:
233 |                 status = task["status"]
234 |             
235 |             # Update the task
236 |             self.terminal_ui.api.update_task(
237 |                 task["id"],
238 |                 title=title,
239 |                 description=description,
240 |                 priority=priority,
241 |                 status=status
242 |             )
243 |             
244 |             # Refresh task list and details
245 |             self.terminal_ui.refresh_tasks()
246 |             self._update_task_details()
247 |     
248 |     def _delete_task(self):
249 |         """Delete the selected task."""
250 |         task = self.terminal_ui.task_list_win.get_selected_task()
251 |         if not task:
252 |             return
253 |         
254 |         confirm = self.terminal_ui.show_confirm_dialog(
255 |             "Delete Task",
256 |             f"Are you sure you want to delete the task '{task['title']}'?"
257 |         )
258 |         
259 |         if confirm:
260 |             # Delete the task
261 |             self.terminal_ui.api.delete_task(task["id"])
262 |             
263 |             # Refresh task list
264 |             self.terminal_ui.refresh_tasks()
265 |             self._update_task_details()
266 |     
267 |     def _new_plan_step(self):
268 |         """Create a new plan step."""
269 |         prompts = ["Name", "Description", "Details"]
270 |         values = self.terminal_ui.show_input_dialog("New Plan Step", prompts)
271 |         
272 |         try:
273 |             if values and values[0]:  # At least the name should be provided
274 |                 # Add the plan step
275 |                 name = values[0]
276 |                 description = values[1] if len(values) > 1 else ""
277 |                 details = values[2] if len(values) > 2 else ""
278 |                 
279 |                 step = self.terminal_ui.api.add_plan_step(
280 |                     name=name,
281 |                     description=description,
282 |                     details=details
283 |                 )
284 |                 
285 |                 # Refresh plan
286 |                 self.terminal_ui.refresh_plan()
287 |                 
288 |                 # Only try to find and select the new step if it was successfully created
289 |                 if step and isinstance(step, dict) and "id" in step:
290 |                     steps = self.terminal_ui.api.get_all_plan_steps()
291 |                     for i, s in enumerate(steps):
292 |                         if s["id"] == step["id"]:
293 |                             self.terminal_ui.plan_win.selected_index = i
294 |                             self.terminal_ui.plan_win.adjust_selection()
295 |                             self.terminal_ui.plan_win.refresh_content()
296 |                             break
297 |         except Exception as e:
298 |             self.terminal_ui.show_message(f"Error creating plan step: {str(e)}")
299 |     
300 |     def _edit_plan_step(self):
301 |         """Edit the selected plan step."""
302 |         step = self.terminal_ui.plan_win.get_selected_step()
303 |         if not step:
304 |             return
305 |         
306 |         # Set up the edit dialog with current values
307 |         prompts = ["Name", "Description", "Details", "Order"]
308 |         values = [
309 |             step.get("name", step.get("description", "")),
310 |             step.get("description", ""),
311 |             step.get("details", ""),
312 |             str(step.get("order", 0))
313 |         ]
314 |         
315 |         new_values = self.terminal_ui.show_input_dialog("Edit Plan Step", prompts, values)
316 |         
317 |         if new_values:
318 |             # Extract and validate values
319 |             name = new_values[0] if len(new_values) > 0 else ""
320 |             description = new_values[1] if len(new_values) > 1 else ""
321 |             details = new_values[2] if len(new_values) > 2 else ""
322 |             order_str = new_values[3] if len(new_values) > 3 else ""
323 |             
324 |             # Validate order
325 |             try:
326 |                 order = int(order_str) if order_str else step.get("order", 0)
327 |                 if order < 0:
328 |                     order = step.get("order", 0)
329 |             except ValueError:
330 |                 order = step.get("order", 0)
331 |             
332 |             # Update the plan step
333 |             self.terminal_ui.api.update_plan_step(
334 |                 step["id"],
335 |                 name=name,
336 |                 description=description,
337 |                 details=details,
338 |                 order=order
339 |             )
340 |             
341 |             # Refresh plan
342 |             self.terminal_ui.refresh_plan()
343 |     
344 |     def _delete_plan_step(self):
345 |         """Delete the selected plan step."""
346 |         step = self.terminal_ui.plan_win.get_selected_step()
347 |         if not step:
348 |             return
349 |         
350 |         confirm = self.terminal_ui.show_confirm_dialog(
351 |             "Delete Plan Step",
352 |             f"Are you sure you want to delete the plan step '{step['description']}'?"
353 |         )
354 |         
355 |         if confirm:
356 |             # Delete the plan step
357 |             self.terminal_ui.api.delete_plan_step(step["id"])
358 |             
359 |             # Refresh plan
360 |             self.terminal_ui.refresh_plan()
361 |     
362 |     def _toggle_selected_task(self):
363 |         """Cycle through task statuses (not_started -> in_progress -> completed -> not_started)."""
364 |         task = self.terminal_ui.task_list_win.get_selected_task()
365 |         if not task:
366 |             return
367 |         
368 |         # Cycle through the statuses
369 |         current_status = task.get("status", "not_started")
370 |         
371 |         # If it's an old "pending" status, treat it as "not_started"
372 |         if current_status == "pending":
373 |             current_status = "not_started"
374 |             
375 |         status_cycle = {
376 |             "not_started": "in_progress",
377 |             "in_progress": "completed",
378 |             "completed": "not_started"
379 |         }
380 |         
381 |         new_status = status_cycle.get(current_status, "not_started")
382 |         
383 |         # Update the task
384 |         self.terminal_ui.api.update_task(
385 |             task["id"],
386 |             status=new_status
387 |         )
388 |         
389 |         # Refresh task list
390 |         self.terminal_ui.refresh_tasks()
391 |     
392 |     def _toggle_plan_step(self):
393 |         """Toggle the completion status of the selected plan step."""
394 |         step = self.terminal_ui.plan_win.get_selected_step()
395 |         if not step:
396 |             return
397 |         
398 |         # Toggle the plan step
399 |         self.terminal_ui.api.toggle_plan_step(step["id"])
400 |         
401 |         # Refresh plan
402 |         self.terminal_ui.refresh_plan()
```

--------------------------------------------------------------------------------
/mcp_server_fixed.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | MCP-compatible server for the Terminal Task Tracker
  4 | 
  5 | This server exposes the task tracker functionality through the Model Context Protocol (MCP).
  6 | """
  7 | import json
  8 | import os
  9 | import logging
 10 | from typing import Dict, List, Optional, Any, Union, AsyncIterator
 11 | from contextlib import asynccontextmanager
 12 | from collections.abc import AsyncIterator
 13 | 
 14 | from mcp.server.fastmcp import FastMCP, Context, Image
 15 | from app.core.task_manager import TaskManager
 16 | from app.core.plan_manager import PlanManager
 17 | from app.api.api import TaskTrackerAPI
 18 | 
 19 | # Set up logging
 20 | logging.basicConfig(level=logging.INFO, 
 21 |                     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 22 | logger = logging.getLogger(__name__)
 23 | 
 24 | # Initialize data files
 25 | home_dir = os.path.expanduser("~")
 26 | data_dir = os.path.join(home_dir, ".tasktracker")
 27 | os.makedirs(data_dir, exist_ok=True)
 28 | task_file = os.path.join(data_dir, "tasks.json")
 29 | plan_file = os.path.join(data_dir, "plan.json")
 30 | notes_file = os.path.join(data_dir, "notes.txt")
 31 | 
 32 | # Global variable for API access from resources without URI parameters
 33 | global_api = None
 34 | 
 35 | # Set up lifespan context manager for the MCP server
 36 | @asynccontextmanager
 37 | async def lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
 38 |     """
 39 |     Initialize and manage the task and plan managers for the server's lifespan.
 40 |     
 41 |     This ensures that we have a single consistent instance of the managers
 42 |     throughout the server's lifecycle.
 43 |     """
 44 |     logger.info(f"Starting TaskTracker server with data directory: {data_dir}")
 45 |     
 46 |     # Initialize managers with explicit file paths
 47 |     task_manager = TaskManager(task_file, notes_file)
 48 |     plan_manager = PlanManager(plan_file)
 49 |     
 50 |     # Create API
 51 |     api = TaskTrackerAPI(task_manager, plan_manager)
 52 |     
 53 |     # Set global API for resources without URI parameters
 54 |     global global_api
 55 |     global_api = api
 56 |     
 57 |     try:
 58 |         # Yield the API instance to the server
 59 |         yield {"api": api}
 60 |     finally:
 61 |         # Ensure all data is saved on shutdown
 62 |         logger.info("Shutting down TaskTracker server, saving all data")
 63 |         api.save_all()
 64 | 
 65 | # Create an MCP server with the lifespan manager
 66 | mcp = FastMCP("TaskTracker", lifespan=lifespan)
 67 | 
 68 | 
 69 | # === Resources ===
 70 | 
 71 | @mcp.resource("tasks://all")
 72 | def get_all_tasks() -> str:
 73 |     """Get all tasks in the system as JSON."""
 74 |     # Reload data from files first to ensure we have latest changes
 75 |     global_api.reload_all()
 76 |     tasks = global_api.get_all_tasks()
 77 |     return json.dumps(tasks, indent=2)
 78 | 
 79 | 
 80 | @mcp.resource("tasks://{task_id}")
 81 | def get_task(task_id: str) -> str:
 82 |     """Get a specific task by ID."""
 83 |     # Reload data from files first to ensure we have latest changes
 84 |     global_api.reload_all()
 85 |     task = global_api.get_task(task_id)
 86 |     if task:
 87 |         return json.dumps(task, indent=2)
 88 |     return "Task not found"
 89 | 
 90 | 
 91 | @mcp.resource("plan://all")
 92 | def get_all_plan_steps() -> str:
 93 |     """Get all plan steps in the system as JSON."""
 94 |     # Reload data from files first to ensure we have latest changes
 95 |     global_api.reload_all()
 96 |     steps = global_api.get_all_plan_steps()
 97 |     return json.dumps(steps, indent=2)
 98 | 
 99 | 
100 | @mcp.resource("plan://{step_id}")
101 | def get_plan_step(step_id: str) -> str:
102 |     """Get a specific plan step by ID."""
103 |     # Reload data from files first to ensure we have latest changes
104 |     global_api.reload_all()
105 |     step = global_api.get_plan_step(step_id)
106 |     if step:
107 |         return json.dumps(step, indent=2)
108 |     return "Plan step not found"
109 | 
110 | 
111 | @mcp.resource("notes://all")
112 | def get_notes() -> str:
113 |     """Get all notes in the system."""
114 |     # Reload data from files first to ensure we have latest changes
115 |     global_api.reload_all()
116 |     return global_api.get_notes()
117 | 
118 | 
119 | # === Tools ===
120 | 
121 | @mcp.tool()
122 | def get_all_tasks_tool(ctx: Context) -> List[Dict[str, Any]]:
123 |     """
124 |     Get all tasks in the system.
125 |     
126 |     Returns:
127 |         List of all tasks
128 |     """
129 |     api = ctx.request_context.lifespan_context["api"]
130 |     # Reload data from files first to ensure we have latest changes
131 |     api.reload_all()
132 |     return api.get_all_tasks()
133 | 
134 | 
135 | @mcp.tool()
136 | def get_task_tool(task_id: str, ctx: Context) -> Dict[str, Any]:
137 |     """
138 |     Get a specific task by ID.
139 |     
140 |     Args:
141 |         task_id: The ID of the task to retrieve
142 |         
143 |     Returns:
144 |         The task or an error message if not found
145 |     """
146 |     api = ctx.request_context.lifespan_context["api"]
147 |     # Reload data from files first to ensure we have latest changes
148 |     api.reload_all()
149 |     task = api.get_task(task_id)
150 |     if task:
151 |         return task
152 |     return {"error": "Task not found"}
153 | 
154 | 
155 | @mcp.tool()
156 | def get_all_plan_steps_tool(ctx: Context) -> List[Dict[str, Any]]:
157 |     """
158 |     Get all plan steps in the system.
159 |     
160 |     Returns:
161 |         List of all plan steps
162 |     """
163 |     api = ctx.request_context.lifespan_context["api"]
164 |     # Reload data from files first to ensure we have latest changes
165 |     api.reload_all()
166 |     return api.get_all_plan_steps()
167 | 
168 | 
169 | @mcp.tool()
170 | def get_plan_step_tool(step_id: str, ctx: Context) -> Dict[str, Any]:
171 |     """
172 |     Get a specific plan step by ID.
173 |     
174 |     Args:
175 |         step_id: The ID of the plan step to retrieve
176 |         
177 |     Returns:
178 |         The plan step or an error message if not found
179 |     """
180 |     api = ctx.request_context.lifespan_context["api"]
181 |     # Reload data from files first to ensure we have latest changes
182 |     api.reload_all()
183 |     step = api.get_plan_step(step_id)
184 |     if step:
185 |         return step
186 |     return {"error": "Plan step not found"}
187 | 
188 | 
189 | @mcp.tool()
190 | def get_notes_tool(ctx: Context) -> str:
191 |     """
192 |     Get all notes in the system.
193 |     
194 |     Returns:
195 |         The notes text
196 |     """
197 |     api = ctx.request_context.lifespan_context["api"]
198 |     # Reload data from files first to ensure we have latest changes
199 |     api.reload_all()
200 |     return api.get_notes()
201 | 
202 | 
203 | @mcp.tool()
204 | def add_task(title: str, ctx: Context, description: str = "", priority: int = 1, 
205 |              status: str = "not_started") -> Dict[str, Any]:
206 |     """
207 |     Add a new task to the system.
208 |     
209 |     Args:
210 |         title: The title of the task
211 |         ctx: The MCP context object
212 |         description: A detailed description of the task
213 |         priority: Priority level (1-3, with 1 being highest)
214 |         status: Current status (not_started, in_progress, completed)
215 |         
216 |     Returns:
217 |         The newly created task
218 |     """
219 |     api = ctx.request_context.lifespan_context["api"]
220 |     # Reload data from files first to ensure we have latest changes
221 |     api.reload_all()
222 |     task = api.add_task(title, description, priority, status)
223 |     api.save_all()
224 |     logger.info(f"Added task: {title} (ID: {task['id']})")
225 |     return task
226 | 
227 | 
228 | @mcp.tool()
229 | def update_task(task_id: str, ctx: Context, title: Optional[str] = None, 
230 |                 description: Optional[str] = None, priority: Optional[int] = None,
231 |                 status: Optional[str] = None) -> Dict[str, Any]:
232 |     """
233 |     Update an existing task.
234 |     
235 |     Args:
236 |         task_id: The ID of the task to update
237 |         title: New title (optional)
238 |         description: New description (optional)
239 |         priority: New priority (optional)
240 |         status: New status (optional)
241 |         
242 |     Returns:
243 |         The updated task or None if task not found
244 |     """
245 |     api = ctx.request_context.lifespan_context["api"]
246 |     # Reload data from files first to ensure we have latest changes
247 |     api.reload_all()
248 |     
249 |     kwargs = {}
250 |     if title is not None:
251 |         kwargs["title"] = title
252 |     if description is not None:
253 |         kwargs["description"] = description
254 |     if priority is not None:
255 |         kwargs["priority"] = priority
256 |     if status is not None:
257 |         kwargs["status"] = status
258 |         
259 |     task = api.update_task(task_id, **kwargs)
260 |     if task:
261 |         api.save_all()
262 |         logger.info(f"Updated task ID: {task_id}")
263 |     else:
264 |         logger.warning(f"Failed to update task: {task_id} - Not found")
265 |     return task or {"error": "Task not found"}
266 | 
267 | 
268 | @mcp.tool()
269 | def delete_task(task_id: str, ctx: Context) -> Dict[str, Any]:
270 |     """
271 |     Delete a task.
272 |     
273 |     Args:
274 |         task_id: The ID of the task to delete
275 |         
276 |     Returns:
277 |         Success or failure message
278 |     """
279 |     api = ctx.request_context.lifespan_context["api"]
280 |     # Reload data from files first to ensure we have latest changes
281 |     api.reload_all()
282 |     
283 |     result = api.delete_task(task_id)
284 |     if result:
285 |         api.save_all()
286 |         logger.info(f"Deleted task ID: {task_id}")
287 |         return {"success": True, "message": "Task deleted successfully"}
288 |     logger.warning(f"Failed to delete task: {task_id} - Not found")
289 |     return {"success": False, "message": "Task not found"}
290 | 
291 | 
292 | @mcp.tool()
293 | def add_plan_step(name: str, ctx: Context, description: str = "", details: str = "",
294 |                   order: Optional[int] = None, completed: bool = False) -> Dict[str, Any]:
295 |     """
296 |     Add a new plan step.
297 |     
298 |     Args:
299 |         name: The name of the plan step
300 |         description: A brief description
301 |         details: Detailed information about the step
302 |         order: Position in the plan (optional)
303 |         completed: Whether the step is completed
304 |         
305 |     Returns:
306 |         The newly created plan step
307 |     """
308 |     api = ctx.request_context.lifespan_context["api"]
309 |     # Reload data from files first to ensure we have latest changes
310 |     api.reload_all()
311 |     
312 |     step = api.add_plan_step(name, description, details, order, completed)
313 |     api.save_all()
314 |     logger.info(f"Added plan step: {name} (ID: {step['id']})")
315 |     return step
316 | 
317 | 
318 | @mcp.tool()
319 | def update_plan_step(step_id: str, ctx: Context, name: Optional[str] = None,
320 |                      description: Optional[str] = None, details: Optional[str] = None,
321 |                      order: Optional[int] = None, completed: Optional[bool] = None) -> Dict[str, Any]:
322 |     """
323 |     Update an existing plan step.
324 |     
325 |     Args:
326 |         step_id: The ID of the step to update
327 |         name: New name (optional)
328 |         description: New description (optional)
329 |         details: New details (optional)
330 |         order: New order (optional)
331 |         completed: New completion status (optional)
332 |         
333 |     Returns:
334 |         The updated plan step or None if not found
335 |     """
336 |     api = ctx.request_context.lifespan_context["api"]
337 |     # Reload data from files first to ensure we have latest changes
338 |     api.reload_all()
339 |     
340 |     kwargs = {}
341 |     if name is not None:
342 |         kwargs["name"] = name
343 |     if description is not None:
344 |         kwargs["description"] = description
345 |     if details is not None:
346 |         kwargs["details"] = details
347 |     if order is not None:
348 |         kwargs["order"] = order
349 |     if completed is not None:
350 |         kwargs["completed"] = completed
351 |         
352 |     step = api.update_plan_step(step_id, **kwargs)
353 |     if step:
354 |         api.save_all()
355 |         logger.info(f"Updated plan step ID: {step_id}")
356 |     else:
357 |         logger.warning(f"Failed to update plan step: {step_id} - Not found")
358 |     return step or {"error": "Plan step not found"}
359 | 
360 | 
361 | @mcp.tool()
362 | def delete_plan_step(step_id: str, ctx: Context) -> Dict[str, Any]:
363 |     """
364 |     Delete a plan step.
365 |     
366 |     Args:
367 |         step_id: The ID of the step to delete
368 |         
369 |     Returns:
370 |         Success or failure message
371 |     """
372 |     api = ctx.request_context.lifespan_context["api"]
373 |     # Reload data from files first to ensure we have latest changes
374 |     api.reload_all()
375 |     
376 |     result = api.delete_plan_step(step_id)
377 |     if result:
378 |         api.save_all()
379 |         logger.info(f"Deleted plan step ID: {step_id}")
380 |         return {"success": True, "message": "Plan step deleted successfully"}
381 |     logger.warning(f"Failed to delete plan step: {step_id} - Not found")
382 |     return {"success": False, "message": "Plan step not found"}
383 | 
384 | 
385 | @mcp.tool()
386 | def toggle_plan_step(step_id: str, ctx: Context) -> Dict[str, Any]:
387 |     """
388 |     Toggle the completion status of a plan step.
389 |     
390 |     Args:
391 |         step_id: The ID of the step to toggle
392 |         
393 |     Returns:
394 |         The updated plan step or None if not found
395 |     """
396 |     api = ctx.request_context.lifespan_context["api"]
397 |     # Reload data from files first to ensure we have latest changes
398 |     api.reload_all()
399 |     
400 |     step = api.toggle_plan_step(step_id)
401 |     if step:
402 |         api.save_all()
403 |         logger.info(f"Toggled completion status of plan step ID: {step_id} to {step['completed']}")
404 |     else:
405 |         logger.warning(f"Failed to toggle plan step: {step_id} - Not found")
406 |     return step or {"error": "Plan step not found"}
407 | 
408 | 
409 | @mcp.tool()
410 | def save_notes(notes_text: str, ctx: Context) -> Dict[str, Any]:
411 |     """
412 |     Save notes to the system.
413 |     
414 |     Args:
415 |         notes_text: The notes text to save
416 |         
417 |     Returns:
418 |         Success message
419 |     """
420 |     api = ctx.request_context.lifespan_context["api"]
421 |     # Reload data from files first to ensure we have latest changes
422 |     api.reload_all()
423 |     
424 |     api.save_notes(notes_text)
425 |     api.save_all()
426 |     logger.info("Notes saved")
427 |     return {"success": True, "message": "Notes saved successfully"}
428 | 
429 | 
430 | @mcp.tool()
431 | def export_data(file_path: str, ctx: Context) -> Dict[str, Any]:
432 |     """
433 |     Export all data to a JSON file.
434 |     
435 |     Args:
436 |         file_path: Path to save the exported data
437 |         
438 |     Returns:
439 |         Success or failure message
440 |     """
441 |     api = ctx.request_context.lifespan_context["api"]
442 |     # Reload data from files first to ensure we have latest changes
443 |     api.reload_all()
444 |     
445 |     try:
446 |         api.export_data(file_path)
447 |         logger.info(f"Data exported to {file_path}")
448 |         return {"success": True, "message": f"Data exported to {file_path}"}
449 |     except Exception as e:
450 |         logger.error(f"Export failed: {str(e)}")
451 |         return {"success": False, "message": f"Export failed: {str(e)}"}
452 | 
453 | 
454 | @mcp.tool()
455 | def import_data(file_path: str, ctx: Context) -> Dict[str, Any]:
456 |     """
457 |     Import data from a JSON file.
458 |     
459 |     Args:
460 |         file_path: Path to the file containing the data to import
461 |         
462 |     Returns:
463 |         Success or failure message
464 |     """
465 |     api = ctx.request_context.lifespan_context["api"]
466 |     # Reload data from files first to ensure we have latest changes
467 |     api.reload_all()
468 |     
469 |     try:
470 |         result = api.import_data(file_path)
471 |         if result:
472 |             logger.info(f"Data imported from {file_path}")
473 |             return {"success": True, "message": "Data imported successfully"}
474 |         logger.warning(f"Import failed from {file_path}")
475 |         return {"success": False, "message": "Import failed"}
476 |     except Exception as e:
477 |         logger.error(f"Import failed: {str(e)}")
478 |         return {"success": False, "message": f"Import failed: {str(e)}"}
479 | 
480 | 
481 | # === Prompts ===
482 | 
483 | @mcp.prompt()
484 | def add_task_prompt(title: str = "", description: str = "") -> str:
485 |     """Create a prompt to add a new task."""
486 |     return f"""Please add a new task with the following details:
487 | Title: {title}
488 | Description: {description}
489 | 
490 | Please provide any missing information and set the priority and status.
491 | """
492 | 
493 | 
494 | @mcp.prompt()
495 | def create_plan_prompt() -> str:
496 |     """Create a prompt to help create a new project plan."""
497 |     return """I need to create a new project plan. Please help me break down this project into clear steps.
498 | 
499 | For each step, I need:
500 | 1. A clear name
501 | 2. A brief description
502 | 3. Any detailed information needed to complete the step
503 | 4. The logical order of the steps
504 | 
505 | Please ask me about my project goals so you can help create an appropriate plan.
506 | """
507 | 
508 | 
509 | # Define a main function for entry point
510 | def main():
511 |     """Run the MCP server."""
512 |     mcp.run()
513 | 
514 | 
515 | # Run the server if executed directly
516 | if __name__ == "__main__":
517 |     main()
```

--------------------------------------------------------------------------------
/app/ui/ui_components.py:
--------------------------------------------------------------------------------

```python
  1 | import curses
  2 | 
  3 | class Window:
  4 |     def __init__(self, stdscr, height, width, y, x, title=""):
  5 |         """Initialize a window with a border and optional title."""
  6 |         self.win = stdscr.subwin(height, width, y, x)
  7 |         self.height = height
  8 |         self.width = width
  9 |         self.title = title
 10 |         self.win.box()
 11 |         self.set_title(title)
 12 |         self.content_window = self.win.derwin(height - 2, width - 2, 1, 1)
 13 |         self.selected_index = 0
 14 |         self.scroll_offset = 0
 15 |         self.max_visible_items = height - 2
 16 |     
 17 |     def set_title(self, title):
 18 |         """Set the window title."""
 19 |         self.title = title
 20 |         if title:
 21 |             title_str = f" {title} "
 22 |             x = max(1, (self.width - len(title_str)) // 2)
 23 |             try:
 24 |                 self.win.addstr(0, x, title_str)
 25 |             except curses.error:
 26 |                 # Fall back to a simpler title if there's an error
 27 |                 try:
 28 |                     self.win.addstr(0, 1, "Window")
 29 |                 except:
 30 |                     pass
 31 |     
 32 |     def clear(self):
 33 |         """Clear the content window."""
 34 |         self.content_window.clear()
 35 |     
 36 |     def refresh(self):
 37 |         """Refresh the window and its content."""
 38 |         self.win.box()
 39 |         self.set_title(self.title)
 40 |         self.win.refresh()
 41 |         self.content_window.refresh()
 42 |     
 43 |     def get_content_dimensions(self):
 44 |         """Get the usable dimensions of the content window."""
 45 |         return self.height - 2, self.width - 2
 46 |     
 47 |     def resize(self, height, width, y, x):
 48 |         """Resize and move the window."""
 49 |         self.height = height
 50 |         self.width = width
 51 |         self.win.resize(height, width)
 52 |         self.win.mvwin(y, x)
 53 |         self.content_window = self.win.derwin(height - 2, width - 2, 1, 1)
 54 |         self.max_visible_items = height - 2
 55 |         self.refresh()
 56 |     
 57 |     def display_message(self, message):
 58 |         """Display a message in the content window."""
 59 |         self.clear()
 60 |         self.content_window.addstr(0, 0, message)
 61 |         self.refresh()
 62 | 
 63 | 
 64 | class TaskListWindow(Window):
 65 |     def __init__(self, stdscr, height, width, y, x, title="Tasks"):
 66 |         """Initialize a task list window."""
 67 |         super().__init__(stdscr, height, width, y, x, title)
 68 |         self.tasks = []
 69 |     
 70 |     def set_tasks(self, tasks):
 71 |         """Set the tasks to display."""
 72 |         self.tasks = tasks
 73 |         self.adjust_selection()
 74 |         self.refresh_content()
 75 |     
 76 |     def adjust_selection(self):
 77 |         """Adjust selection index and scroll offset to valid values."""
 78 |         if not self.tasks:
 79 |             self.selected_index = 0
 80 |             self.scroll_offset = 0
 81 |             return
 82 |         
 83 |         # Ensure selected_index is in valid range
 84 |         if self.selected_index >= len(self.tasks):
 85 |             self.selected_index = len(self.tasks) - 1
 86 |         
 87 |         # Adjust scroll offset to keep selected item visible
 88 |         if self.selected_index < self.scroll_offset:
 89 |             self.scroll_offset = self.selected_index
 90 |         elif self.selected_index >= self.scroll_offset + self.max_visible_items:
 91 |             self.scroll_offset = self.selected_index - self.max_visible_items + 1
 92 |     
 93 |     def refresh_content(self):
 94 |         """Refresh the task list content."""
 95 |         self.clear()
 96 |         content_height, content_width = self.get_content_dimensions()
 97 |         
 98 |         if not self.tasks:
 99 |             self.content_window.addstr(0, 0, "No tasks")
100 |             self.refresh()
101 |             return
102 |         
103 |         # Display visible tasks
104 |         for i in range(min(self.max_visible_items, len(self.tasks))):
105 |             idx = i + self.scroll_offset
106 |             if idx >= len(self.tasks):
107 |                 break
108 |             
109 |             task = self.tasks[idx]
110 |             
111 |             # Highlight selected task
112 |             if idx == self.selected_index:
113 |                 self.content_window.attron(curses.A_REVERSE)
114 |             
115 |             # Format priority indicator
116 |             priority_markers = ["!", "!!", "!!!"]
117 |             priority_str = priority_markers[task['priority'] - 1] if 1 <= task['priority'] <= 3 else ""
118 |             
119 |             # Format status with icons (using ASCII only to avoid Unicode display issues)
120 |             status_map = {
121 |                 "not_started": "[ ]",  # Empty square
122 |                 "in_progress": "[>]",  # Right arrow (in progress) - ASCII version
123 |                 "completed": "[x]",    # Checkmark - ASCII version
124 |                 # For backward compatibility
125 |                 "pending": "[ ]"
126 |             }
127 |             status_str = status_map.get(task['status'], "[ ]")
128 |             
129 |             # Truncate title if needed
130 |             max_title_width = content_width - len(priority_str) - len(status_str) - 2
131 |             title = task['title']
132 |             if len(title) > max_title_width:
133 |                 title = title[:max_title_width-3] + "..."
134 |             
135 |             # Display task line with error handling
136 |             task_str = f"{status_str} {title} {priority_str}"
137 |             try:
138 |                 self.content_window.addstr(i, 0, task_str)
139 |             except curses.error:
140 |                 # Handle display errors gracefully
141 |                 try:
142 |                     # Try with a simpler string
143 |                     self.content_window.addstr(i, 0, f"Task {i+1}")
144 |                 except:
145 |                     pass
146 |             
147 |             if idx == self.selected_index:
148 |                 self.content_window.attroff(curses.A_REVERSE)
149 |         
150 |         self.refresh()
151 |     
152 |     def select_next(self):
153 |         """Select the next task if available."""
154 |         if self.tasks and self.selected_index < len(self.tasks) - 1:
155 |             self.selected_index += 1
156 |             self.adjust_selection()
157 |             self.refresh_content()
158 |     
159 |     def select_prev(self):
160 |         """Select the previous task if available."""
161 |         if self.tasks and self.selected_index > 0:
162 |             self.selected_index -= 1
163 |             self.adjust_selection()
164 |             self.refresh_content()
165 |     
166 |     def get_selected_task(self):
167 |         """Get the currently selected task."""
168 |         if self.tasks and 0 <= self.selected_index < len(self.tasks):
169 |             return self.tasks[self.selected_index]
170 |         return None
171 | 
172 | 
173 | class TaskDetailWindow(Window):
174 |     def __init__(self, stdscr, height, width, y, x, title="Task Details"):
175 |         """Initialize a task detail window."""
176 |         super().__init__(stdscr, height, width, y, x, title)
177 |         self.task = None
178 |     
179 |     def set_task(self, task):
180 |         """Set the task to display details for."""
181 |         self.task = task
182 |         self.refresh_content()
183 |     
184 |     def refresh_content(self):
185 |         """Refresh the task detail content."""
186 |         self.clear()
187 |         
188 |         if not self.task:
189 |             self.content_window.addstr(0, 0, "No task selected")
190 |             self.refresh()
191 |             return
192 |         
193 |         # Display task properties
194 |         content_height, content_width = self.get_content_dimensions()
195 |         
196 |         # Map priority and status to more readable forms
197 |         priority_map = {1: "Low", 2: "Medium", 3: "High"}
198 |         status_map = {
199 |             "not_started": "Not Started",
200 |             "in_progress": "In Progress",
201 |             "completed": "Completed",
202 |             # For backward compatibility
203 |             "pending": "Not Started"
204 |         }
205 |         
206 |         priority = priority_map.get(self.task['priority'], "Unknown")
207 |         status = status_map.get(self.task['status'], "Unknown")
208 |         
209 |         # Display task details
210 |         y = 0
211 |         self.content_window.addstr(y, 0, f"Title: {self.task['title']}")
212 |         y += 2
213 |         
214 |         self.content_window.addstr(y, 0, f"Status: {status}")
215 |         y += 1
216 |         self.content_window.addstr(y, 0, f"Priority: {priority}")
217 |         y += 2
218 |         
219 |         # Display description with word wrapping
220 |         self.content_window.addstr(y, 0, "Description:")
221 |         y += 1
222 |         
223 |         description = self.task['description'] or "No description provided."
224 |         words = description.split()
225 |         
226 |         if words:
227 |             line = ""
228 |             for word in words:
229 |                 # Check if adding this word would exceed width
230 |                 if len(line) + len(word) + 1 > content_width:
231 |                     self.content_window.addstr(y, 0, line)
232 |                     y += 1
233 |                     line = word
234 |                 else:
235 |                     if line:
236 |                         line += " " + word
237 |                     else:
238 |                         line = word
239 |             
240 |             # Add the last line if it has content
241 |             if line:
242 |                 self.content_window.addstr(y, 0, line)
243 |                 y += 1
244 |         
245 |         # Display created/updated timestamps
246 |         y += 1
247 |         created = self.task.get('created_at', '').split('T')[0]
248 |         updated = self.task.get('updated_at', '').split('T')[0]
249 |         
250 |         if created:
251 |             self.content_window.addstr(y, 0, f"Created: {created}")
252 |             y += 1
253 |         if updated and updated != created:
254 |             self.content_window.addstr(y, 0, f"Updated: {updated}")
255 |         
256 |         self.refresh()
257 | 
258 | 
259 | class NotesWindow(Window):
260 |     def __init__(self, stdscr, height, width, y, x, title="Notes"):
261 |         """Initialize a notes window."""
262 |         super().__init__(stdscr, height, width, y, x, title)
263 |         self.notes = ""
264 |         self.edit_mode = False
265 |         self.cursor_pos = 0
266 |         self.scroll_offset = 0
267 |         
268 |         # Enable keypad for special key handling
269 |         self.content_window.keypad(True)
270 |     
271 |     def set_notes(self, notes):
272 |         """Set the notes to display."""
273 |         self.notes = notes if notes else ""
274 |         self.refresh_content()
275 |     
276 |     def get_notes(self):
277 |         """Get the current notes."""
278 |         return self.notes
279 |     
280 |     def toggle_edit_mode(self):
281 |         """Toggle between view and edit mode."""
282 |         self.edit_mode = not self.edit_mode
283 |         if self.edit_mode:
284 |             curses.curs_set(1)  # Show cursor
285 |         else:
286 |             curses.curs_set(0)  # Hide cursor
287 |         self.refresh_content()
288 |         return self.edit_mode
289 |     
290 |     def handle_key(self, key):
291 |         """Handle keyboard input in edit mode."""
292 |         if not self.edit_mode:
293 |             return False
294 |         
295 |         try:
296 |             # Simple key handling - safer approach
297 |             if key in (10, 13, curses.KEY_ENTER):  # Enter
298 |                 # Add a newline at end for simplicity
299 |                 self.notes += "\n"
300 |                 
301 |             elif key in (curses.KEY_BACKSPACE, 127, 8):  # Backspace
302 |                 # Remove last character if there are any
303 |                 if len(self.notes) > 0:
304 |                     self.notes = self.notes[:-1]
305 |                     
306 |             elif 32 <= key <= 126:  # Printable ASCII characters
307 |                 # Add character to the end
308 |                 self.notes += chr(key)
309 |             
310 |             # Refresh after any change
311 |             self.refresh_content()
312 |             return True
313 |             
314 |         except Exception as e:
315 |             # Log error by adding to notes
316 |             self.notes += f"\nError: {str(e)}\n"
317 |             self.refresh_content()
318 |             return True
319 |     
320 |     def adjust_scroll(self):
321 |         """Adjust scroll offset to keep cursor visible."""
322 |         content_height, content_width = self.get_content_dimensions()
323 |         
324 |         # Count lines up to cursor
325 |         lines_to_cursor = self.notes[:self.cursor_pos].count('\n')
326 |         
327 |         # Adjust scroll if cursor is off screen
328 |         if lines_to_cursor < self.scroll_offset:
329 |             self.scroll_offset = lines_to_cursor
330 |         elif lines_to_cursor >= self.scroll_offset + content_height:
331 |             self.scroll_offset = lines_to_cursor - content_height + 1
332 |     
333 |     def refresh_content(self):
334 |         """Refresh the notes content."""
335 |         try:
336 |             self.clear()
337 |             content_height, content_width = self.get_content_dimensions()
338 |             
339 |             # Simplified content display
340 |             if not self.notes:
341 |                 if self.edit_mode:
342 |                     self.content_window.addstr(0, 0, "Type to add notes...")
343 |                 else:
344 |                     self.content_window.addstr(0, 0, "No notes. Press 'e' to edit.")
345 |             else:
346 |                 # Just display the most recent part of notes (last few lines)
347 |                 lines = self.notes.split('\n')
348 |                 
349 |                 # Display only what fits in the window
350 |                 max_lines = min(content_height - 1, len(lines))
351 |                 start_line = max(0, len(lines) - max_lines)
352 |                 
353 |                 for i in range(max_lines):
354 |                     line_idx = start_line + i
355 |                     if line_idx < len(lines):
356 |                         # Truncate line if needed
357 |                         display_line = lines[line_idx]
358 |                         if len(display_line) > content_width - 1:
359 |                             display_line = display_line[:content_width - 1]
360 |                         
361 |                         self.content_window.addstr(i, 0, display_line)
362 |             
363 |             # Add help text at bottom
364 |             if self.edit_mode and content_height > 1:
365 |                 help_text = "Esc: Save & exit edit mode"
366 |                 if len(help_text) > content_width - 1:
367 |                     help_text = help_text[:content_width - 1]
368 |                 self.content_window.addstr(content_height - 1, 0, help_text)
369 |                 
370 |             # In edit mode, position cursor at the end of content
371 |             if self.edit_mode:
372 |                 # Count displayed lines to find end position
373 |                 line_count = min(max_lines if 'max_lines' in locals() else 0, content_height - 1)
374 |                 if line_count > 0:
375 |                     self.content_window.move(line_count - 1, 0)
376 |                 else:
377 |                     self.content_window.move(0, 0)
378 |             
379 |             self.refresh()
380 |         except Exception as e:
381 |             # If there's an error, try a minimal refresh
382 |             try:
383 |                 self.clear()
384 |                 self.content_window.addstr(0, 0, "Notes")
385 |                 self.refresh()
386 |             except:
387 |                 pass
388 | 
389 | 
390 | class PlanWindow(Window):
391 |     def __init__(self, stdscr, height, width, y, x, title="Project Plan"):
392 |         """Initialize a project plan window."""
393 |         super().__init__(stdscr, height, width, y, x, title)
394 |         self.steps = []
395 |         self.selected_index = 0
396 |         self.scroll_offset = 0
397 |         self.show_details = False  # Flag to control if details are shown
398 |     
399 |     def set_steps(self, steps):
400 |         """Set the plan steps to display."""
401 |         self.steps = steps
402 |         self.adjust_selection()
403 |         self.refresh_content()
404 |     
405 |     def adjust_selection(self):
406 |         """Adjust selection index and scroll offset to valid values."""
407 |         if not self.steps:
408 |             self.selected_index = 0
409 |             self.scroll_offset = 0
410 |             return
411 |         
412 |         # Ensure selected_index is in valid range
413 |         if self.selected_index < 0:
414 |             self.selected_index = 0
415 |         elif self.selected_index >= len(self.steps):
416 |             self.selected_index = max(0, len(self.steps) - 1)
417 |         
418 |         # Adjust scroll offset to keep selected item visible
419 |         if self.selected_index < self.scroll_offset:
420 |             self.scroll_offset = self.selected_index
421 |         elif self.selected_index >= self.scroll_offset + self.max_visible_items:
422 |             self.scroll_offset = max(0, self.selected_index - self.max_visible_items + 1)
423 |     
424 |     def refresh_content(self):
425 |         """Refresh the plan content."""
426 |         self.clear()
427 |         content_height, content_width = self.get_content_dimensions()
428 |         
429 |         if not self.steps:
430 |             self.content_window.addstr(0, 0, "No plan steps")
431 |             self.refresh()
432 |             return
433 |             
434 |         selected_step = self.get_selected_step()
435 |         
436 |         # If showing details for the selected step
437 |         if self.show_details and selected_step:
438 |             self._display_step_details(selected_step, content_height, content_width)
439 |             return
440 |             
441 |         # Otherwise display the list of steps
442 |         list_height = min(content_height, len(self.steps))
443 |         
444 |         # Display visible steps
445 |         for i in range(min(self.max_visible_items, len(self.steps))):
446 |             idx = i + self.scroll_offset
447 |             if idx >= len(self.steps):
448 |                 break
449 |             
450 |             try:
451 |                 step = self.steps[idx]
452 |                 
453 |                 # Highlight selected step
454 |                 if idx == self.selected_index:
455 |                     self.content_window.attron(curses.A_REVERSE)
456 |                 
457 |                 # Format step with order and completion status
458 |                 completion_status = "[x]" if step['completed'] else "[ ]"
459 |                 
460 |                 # Get name or fallback to description for backward compatibility
461 |                 name = step.get('name', step.get('description', 'Unnamed step'))
462 |                 
463 |                 # Truncate name if needed
464 |                 max_name_width = content_width - 10
465 |                 if len(name) > max_name_width:
466 |                     name = name[:max_name_width-3] + "..."
467 |                 
468 |                 # Display step line with safe index access
469 |                 order = step.get('order', 0)
470 |                 step_str = f"{order + 1:2d}. {completion_status} {name}"
471 |                 try:
472 |                     self.content_window.addstr(i, 0, step_str)
473 |                 except curses.error:
474 |                     # Handle display errors gracefully
475 |                     try:
476 |                         # Try with a simpler string
477 |                         self.content_window.addstr(i, 0, f"Step {order + 1}")
478 |                     except:
479 |                         pass
480 |                 
481 |                 if idx == self.selected_index:
482 |                     self.content_window.attroff(curses.A_REVERSE)
483 |             except (IndexError, KeyError) as e:
484 |                 # Handle any index errors gracefully
485 |                 self.content_window.addstr(i, 0, f"Error displaying step: {str(e)}")
486 |                 
487 |         # Add a help line at the bottom if there's space
488 |         if content_height > list_height + 1:
489 |             help_text = "Enter/Space: Toggle completion | D: Show/hide details"
490 |             try:
491 |                 self.content_window.addstr(content_height - 1, 0, help_text)
492 |             except curses.error:
493 |                 pass  # Skip help text if it doesn't fit
494 |             
495 |         self.refresh()
496 |         
497 |     def _display_step_details(self, step, height, width):
498 |         """Display detailed information for a plan step."""
499 |         y = 0
500 |         
501 |         # Display step name
502 |         name = step.get('name', 'Unnamed step')
503 |         self.content_window.addstr(y, 0, f"Name: {name}")
504 |         y += 2
505 |         
506 |         # Display completion status
507 |         completed = "Completed" if step.get('completed', False) else "Not completed"
508 |         self.content_window.addstr(y, 0, f"Status: {completed}")
509 |         y += 2
510 |         
511 |         # Display description
512 |         description = step.get('description', '')
513 |         if description:
514 |             self.content_window.addstr(y, 0, "Description:")
515 |             y += 1
516 |             
517 |             # Word wrap description
518 |             words = description.split()
519 |             line = ""
520 |             for word in words:
521 |                 if len(line) + len(word) + 1 > width:
522 |                     self.content_window.addstr(y, 0, line)
523 |                     y += 1
524 |                     line = word
525 |                 else:
526 |                     if line:
527 |                         line += " " + word
528 |                     else:
529 |                         line = word
530 |             
531 |             if line:
532 |                 self.content_window.addstr(y, 0, line)
533 |                 y += 1
534 |             
535 |             y += 1
536 |         
537 |         # Display detailed information
538 |         details = step.get('details', '')
539 |         if details:
540 |             self.content_window.addstr(y, 0, "Details:")
541 |             y += 1
542 |             
543 |             # Word wrap details
544 |             words = details.split()
545 |             line = ""
546 |             for word in words:
547 |                 if len(line) + len(word) + 1 > width:
548 |                     self.content_window.addstr(y, 0, line)
549 |                     y += 1
550 |                     line = word
551 |                 else:
552 |                     if line:
553 |                         line += " " + word
554 |                     else:
555 |                         line = word
556 |             
557 |             if line:
558 |                 self.content_window.addstr(y, 0, line)
559 |                 y += 1
560 |         
561 |         # Add a help line at the bottom
562 |         help_text = "D: Return to plan list"
563 |         self.content_window.addstr(height - 1, 0, help_text)
564 |         
565 |         self.refresh()
566 |     
567 |     def select_next(self):
568 |         """Select the next step if available."""
569 |         if self.steps and self.selected_index < len(self.steps) - 1:
570 |             self.selected_index += 1
571 |             self.adjust_selection()
572 |             self.refresh_content()
573 |     
574 |     def select_prev(self):
575 |         """Select the previous step if available."""
576 |         if self.steps and self.selected_index > 0:
577 |             self.selected_index -= 1
578 |             self.adjust_selection()
579 |             self.refresh_content()
580 |     
581 |     def get_selected_step(self):
582 |         """Get the currently selected plan step."""
583 |         try:
584 |             if self.steps and 0 <= self.selected_index < len(self.steps):
585 |                 return self.steps[self.selected_index]
586 |         except (IndexError, KeyError):
587 |             pass
588 |         return None
589 |         
590 |     def toggle_details(self):
591 |         """Toggle between displaying the step list and the details of the selected step."""
592 |         if self.get_selected_step():
593 |             self.show_details = not self.show_details
594 |             self.refresh_content()
595 |             return True
596 |         return False
597 | 
598 | 
599 | class InputDialog:
600 |     def __init__(self, stdscr, title, prompts, initial_values=None):
601 |         """
602 |         Initialize an input dialog with multiple fields.
603 |         
604 |         Args:
605 |             stdscr: The main curses window
606 |             title: Dialog title
607 |             prompts: List of field prompts
608 |             initial_values: List of initial values for fields (optional)
609 |         """
610 |         self.stdscr = stdscr
611 |         self.title = title
612 |         self.prompts = prompts
613 |         
614 |         # Initialize with empty values or provided initial values
615 |         if initial_values is None:
616 |             self.values = ["" for _ in range(len(prompts))]
617 |         else:
618 |             self.values = initial_values.copy()
619 |         
620 |         # Dialog dimensions
621 |         screen_height, screen_width = stdscr.getmaxyx()
622 |         self.width = min(60, screen_width - 4)
623 |         self.height = len(prompts) * 2 + 4  # 2 lines per field + borders + buttons
624 |         
625 |         # Center dialog
626 |         self.y = (screen_height - self.height) // 2
627 |         self.x = (screen_width - self.width) // 2
628 |         
629 |         # Create window
630 |         self.win = stdscr.subwin(self.height, self.width, self.y, self.x)
631 |         self.win.keypad(True)  # Enable keypad mode for special keys
632 |         self.current_field = 0
633 |         self.cursor_pos = len(self.values[0]) if self.values and self.values[0] else 0
634 |     
635 |     def show(self):
636 |         """Show the dialog and handle input."""
637 |         curses.curs_set(1)  # Show cursor
638 |         
639 |         # Enable special keys like backspace
640 |         self.win.keypad(True)
641 |         
642 |         # Main input loop
643 |         while True:
644 |             self.draw()
645 |             key = self.win.getch()
646 |             
647 |             if key == curses.KEY_ENTER or key == 10 or key == 13:  # Enter (different codes)
648 |                 curses.curs_set(0)  # Hide cursor
649 |                 return self.values
650 |             
651 |             elif key == 27:  # Escape
652 |                 curses.curs_set(0)  # Hide cursor
653 |                 return None
654 |             
655 |             elif key == curses.KEY_UP and self.current_field > 0:
656 |                 self.current_field -= 1
657 |                 self.cursor_pos = len(self.values[self.current_field])
658 |             
659 |             elif key == curses.KEY_DOWN and self.current_field < len(self.prompts) - 1:
660 |                 self.current_field += 1
661 |                 self.cursor_pos = len(self.values[self.current_field])
662 |             
663 |             elif key == 9:  # Tab
664 |                 self.current_field = (self.current_field + 1) % len(self.prompts)
665 |                 self.cursor_pos = len(self.values[self.current_field])
666 |             
667 |             elif key == curses.KEY_LEFT and self.cursor_pos > 0:
668 |                 self.cursor_pos -= 1
669 |             
670 |             elif key == curses.KEY_RIGHT and self.cursor_pos < len(self.values[self.current_field]):
671 |                 self.cursor_pos += 1
672 |             
673 |             elif key in (curses.KEY_BACKSPACE, 127, 8):  # Different backspace codes
674 |                 if self.cursor_pos > 0:
675 |                     self.values[self.current_field] = (
676 |                         self.values[self.current_field][:self.cursor_pos - 1] + 
677 |                         self.values[self.current_field][self.cursor_pos:]
678 |                     )
679 |                     self.cursor_pos -= 1
680 |             
681 |             elif key == curses.KEY_DC:  # Delete
682 |                 if self.cursor_pos < len(self.values[self.current_field]):
683 |                     self.values[self.current_field] = (
684 |                         self.values[self.current_field][:self.cursor_pos] + 
685 |                         self.values[self.current_field][self.cursor_pos + 1:]
686 |                     )
687 |             
688 |             elif 32 <= key <= 126:  # Printable characters
689 |                 self.values[self.current_field] = (
690 |                     self.values[self.current_field][:self.cursor_pos] + 
691 |                     chr(key) + 
692 |                     self.values[self.current_field][self.cursor_pos:]
693 |                 )
694 |                 self.cursor_pos += 1
695 |     
696 |     def draw(self):
697 |         """Draw the dialog box and input fields."""
698 |         self.win.clear()
699 |         self.win.box()
700 |         
701 |         # Draw title
702 |         if self.title:
703 |             title_str = f" {self.title} "
704 |             x = max(1, (self.width - len(title_str)) // 2)
705 |             self.win.addstr(0, x, title_str)
706 |         
707 |         # Draw input fields
708 |         for i, prompt in enumerate(self.prompts):
709 |             y = i * 2 + 1
710 |             self.win.addstr(y, 2, f"{prompt}:")
711 |             
712 |             # Draw input field
713 |             field_x = 2
714 |             field_y = y + 1
715 |             field_width = self.width - 4
716 |             field_value = self.values[i]
717 |             
718 |             # Draw input value
719 |             self.win.addstr(field_y, field_x, field_value)
720 |             
721 |             # Draw cursor if this is the active field
722 |             if i == self.current_field:
723 |                 self.win.move(field_y, field_x + self.cursor_pos)
724 |         
725 |         # Draw instructions
726 |         self.win.addstr(self.height - 1, 2, "Enter: Save | Esc: Cancel")
727 |         
728 |         self.win.refresh()
729 | 
730 | 
731 | class ConfirmDialog:
732 |     def __init__(self, stdscr, title, message):
733 |         """Initialize a confirmation dialog."""
734 |         self.stdscr = stdscr
735 |         self.title = title
736 |         self.message = message
737 |         
738 |         # Dialog dimensions
739 |         screen_height, screen_width = stdscr.getmaxyx()
740 |         self.width = min(50, screen_width - 4)
741 |         
742 |         # Calculate height based on message length
743 |         message_lines = (len(message) // (self.width - 4)) + 1
744 |         self.height = message_lines + 4  # Message + borders + buttons
745 |         
746 |         # Center dialog
747 |         self.y = (screen_height - self.height) // 2
748 |         self.x = (screen_width - self.width) // 2
749 |         
750 |         # Create window
751 |         self.win = stdscr.subwin(self.height, self.width, self.y, self.x)
752 |         self.selected = 0  # 0 = No, 1 = Yes
753 |     
754 |     def show(self):
755 |         """Show the dialog and handle input."""
756 |         # Enable keypad mode for special keys
757 |         self.win.keypad(True)
758 |         
759 |         # Main input loop
760 |         while True:
761 |             self.draw()
762 |             key = self.win.getch()
763 |             
764 |             if key == curses.KEY_ENTER or key == 10 or key == 13:  # Enter (different codes)
765 |                 return self.selected == 1  # Return True if "Yes" selected
766 |             
767 |             elif key == 27:  # Escape
768 |                 return False
769 |             
770 |             elif key == curses.KEY_LEFT or key == curses.KEY_RIGHT:
771 |                 self.selected = 1 - self.selected  # Toggle between 0 and 1
772 |                 
773 |             # Add handling for y/n keys
774 |             elif key in (ord('y'), ord('Y')):
775 |                 return True
776 |                 
777 |             elif key in (ord('n'), ord('N')):
778 |                 return False
779 |     
780 |     def draw(self):
781 |         """Draw the confirmation dialog."""
782 |         self.win.clear()
783 |         self.win.box()
784 |         
785 |         # Draw title
786 |         if self.title:
787 |             title_str = f" {self.title} "
788 |             x = max(1, (self.width - len(title_str)) // 2)
789 |             self.win.addstr(0, x, title_str)
790 |         
791 |         # Draw message
792 |         message_words = self.message.split()
793 |         line = ""
794 |         y = 1
795 |         
796 |         for word in message_words:
797 |             if len(line) + len(word) + 1 <= self.width - 4:
798 |                 if line:
799 |                     line += " " + word
800 |                 else:
801 |                     line = word
802 |             else:
803 |                 self.win.addstr(y, 2, line)
804 |                 y += 1
805 |                 line = word
806 |         
807 |         if line:
808 |             self.win.addstr(y, 2, line)
809 |         
810 |         # Draw buttons
811 |         button_y = self.height - 2
812 |         no_x = self.width // 3 - 2
813 |         yes_x = 2 * self.width // 3 - 2
814 |         
815 |         if self.selected == 0:
816 |             self.win.attron(curses.A_REVERSE)
817 |         self.win.addstr(button_y, no_x, " No ")
818 |         if self.selected == 0:
819 |             self.win.attroff(curses.A_REVERSE)
820 |         
821 |         if self.selected == 1:
822 |             self.win.attron(curses.A_REVERSE)
823 |         self.win.addstr(button_y, yes_x, " Yes ")
824 |         if self.selected == 1:
825 |             self.win.attroff(curses.A_REVERSE)
826 |         
827 |         self.win.refresh()
```
Page 1/2FirstPrevNextLast