parent
e6f564ac99
commit
7a966db408
@ -0,0 +1,321 @@
|
||||
# main.py - The Controller Layer / Entry Point
|
||||
# Initializes the application, connects the TaskService (Model) and
|
||||
# the UIManager (View), and runs the main event loop.
|
||||
|
||||
from unicurses import *
|
||||
from .task_service import TaskService
|
||||
from .ui_manager import UIManager
|
||||
import sys
|
||||
from dateutil.parser import ParserError, isoparse
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from unicurses import wrapper
|
||||
|
||||
def open_editor_for_task_notes(stdscr, app_state, ui_manager):
|
||||
"""Opens an editor for the task notes."""
|
||||
selected_task = app_state.tasks[ui_manager.selected_task_idx]
|
||||
initial_content = selected_task.get('notes', '')
|
||||
|
||||
editor = os.environ.get('EDITOR', 'vim') # Default to vim
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".tmp", delete=False, mode='w+', encoding='utf-8') as tf:
|
||||
tf.write(initial_content)
|
||||
temp_path = tf.name
|
||||
|
||||
# Suspend curses and open the editor
|
||||
def_prog_mode()
|
||||
endwin()
|
||||
subprocess.call([editor, temp_path])
|
||||
# Resume curses
|
||||
reset_prog_mode()
|
||||
doupdate()
|
||||
|
||||
with open(temp_path, 'r', encoding='utf-8') as tf:
|
||||
new_note = tf.read()
|
||||
|
||||
os.remove(temp_path)
|
||||
|
||||
if new_note != initial_content:
|
||||
app_state.service.change_detail_task(app_state.active_list_id, selected_task['id'], new_note)
|
||||
app_state.refresh_data()
|
||||
|
||||
def is_valid_date(date_str):
|
||||
try:
|
||||
isoparse(date_str)
|
||||
return True
|
||||
except (ParserError, ValueError):
|
||||
return False
|
||||
|
||||
# Global State Management (simplified for TUI)
|
||||
class AppState:
|
||||
"""Holds the application's current state and data."""
|
||||
def __init__(self, task_service):
|
||||
self.service = task_service
|
||||
self.task_lists = self.service.get_task_lists()
|
||||
self.active_list_id = self.service.active_list_id
|
||||
self.current_parent_task_id = None
|
||||
self.filtered_tasks_cache = {} # Cache for filtered tasks
|
||||
self.task_counts = {}
|
||||
self.tasks = self.get_tasks_for_active_list()
|
||||
self.list_buffer = ""
|
||||
self.task_buffer = ""
|
||||
self.parent_task_id_stack = []
|
||||
self.parent_task_idx_stack = []
|
||||
self.calculate_task_counts()
|
||||
self.show_help = False
|
||||
|
||||
def calculate_task_counts(self):
|
||||
"""Calculates the number of tasks in each list."""
|
||||
for task_list in self.task_lists:
|
||||
list_id = task_list['id']
|
||||
self.task_counts[list_id] = len(self.service.get_tasks_for_list(list_id))
|
||||
|
||||
def get_tasks_for_active_list(self):
|
||||
"""Retrieves tasks for the active list, using cache if possible."""
|
||||
if self.current_parent_task_id:
|
||||
return self.service.get_subtasks(self.active_list_id, self.current_parent_task_id)
|
||||
else:
|
||||
if self.active_list_id not in self.filtered_tasks_cache or self.service.dirty:
|
||||
# If not in cache or data is dirty, fetch and cache it
|
||||
tasks = self.service.get_tasks_for_list(self.active_list_id)
|
||||
self.filtered_tasks_cache[self.active_list_id] = tasks
|
||||
return self.filtered_tasks_cache[self.active_list_id]
|
||||
|
||||
def refresh_data(self):
|
||||
"""Refreshes all data from the service layer and clears the cache."""
|
||||
self.task_lists = self.service.get_task_lists()
|
||||
self.filtered_tasks_cache.clear() # Invalidate the cache
|
||||
self.tasks = self.get_tasks_for_active_list()
|
||||
self.calculate_task_counts()
|
||||
|
||||
def change_active_list(self, list_id):
|
||||
"""Updates the active list and fetches new tasks, using the cache."""
|
||||
if self.service.set_active_list(list_id):
|
||||
self.active_list_id = list_id
|
||||
self.current_parent_task_id = None
|
||||
self.tasks = self.get_tasks_for_active_list()
|
||||
return True
|
||||
return False
|
||||
|
||||
def handle_input(stdscr, app_state, ui_manager):
|
||||
"""
|
||||
Main input handler. Maps key presses to application actions.
|
||||
"""
|
||||
try:
|
||||
key = getch()
|
||||
except curses.error:
|
||||
return True # Ignore curses errors on getch()
|
||||
|
||||
# Quitting
|
||||
if key in [ord('q'), ord('Q')]:
|
||||
if app_state.service.dirty:
|
||||
ui_manager.start_sync_animation()
|
||||
app_state.service.sync_to_google()
|
||||
ui_manager.stop_sync_animation()
|
||||
return False
|
||||
|
||||
if key == KEY_RESIZE:
|
||||
return True # Triggers a redraw
|
||||
|
||||
# Movement
|
||||
elif key == KEY_UP or key == ord('k'):
|
||||
if ui_manager.active_panel == 'tasks':
|
||||
ui_manager.update_task_selection(app_state.tasks, -1)
|
||||
elif ui_manager.active_panel == 'lists':
|
||||
ui_manager.update_list_selection(app_state.task_lists, -1)
|
||||
elif key == KEY_DOWN or key == ord('j'):
|
||||
if ui_manager.active_panel == 'tasks':
|
||||
ui_manager.update_task_selection(app_state.tasks, 1)
|
||||
elif ui_manager.active_panel == 'lists':
|
||||
ui_manager.update_list_selection(app_state.task_lists, 1)
|
||||
elif key == KEY_LEFT or key == ord('h'):
|
||||
if app_state.current_parent_task_id:
|
||||
app_state.current_parent_task_id = app_state.parent_task_id_stack.pop()
|
||||
app_state.refresh_data()
|
||||
if app_state.parent_task_idx_stack:
|
||||
ui_manager.selected_task_idx = app_state.parent_task_idx_stack.pop()
|
||||
elif ui_manager.active_panel == 'tasks':
|
||||
ui_manager.toggle_panel()
|
||||
elif key == KEY_RIGHT or key == ord('l'):
|
||||
if ui_manager.active_panel == 'lists':
|
||||
selected_list = app_state.task_lists[ui_manager.selected_list_idx]
|
||||
if app_state.active_list_id != selected_list['id']:
|
||||
app_state.change_active_list(selected_list["id"])
|
||||
ui_manager.selected_task_idx = 0 # Reset task selection
|
||||
ui_manager.toggle_panel()
|
||||
elif ui_manager.active_panel == 'tasks' and app_state.tasks:
|
||||
selected_task = app_state.tasks[ui_manager.selected_task_idx]
|
||||
app_state.parent_task_id_stack.append(app_state.current_parent_task_id)
|
||||
app_state.parent_task_idx_stack.append(ui_manager.selected_task_idx)
|
||||
app_state.current_parent_task_id = selected_task['id']
|
||||
app_state.refresh_data()
|
||||
ui_manager.selected_task_idx = 0
|
||||
|
||||
# Action Keys
|
||||
|
||||
elif key == ord('c'):
|
||||
# Toggle task status
|
||||
selected_task = app_state.tasks[ui_manager.selected_task_idx]
|
||||
app_state.service.toggle_task_status(app_state.active_list_id, selected_task["id"])
|
||||
app_state.refresh_data() # Refresh display after change
|
||||
|
||||
elif key == ord('w'):
|
||||
ui_manager.start_sync_animation()
|
||||
app_state.service.sync_to_google()
|
||||
ui_manager.stop_sync_animation()
|
||||
app_state.refresh_data()
|
||||
|
||||
elif key == ord('r'):
|
||||
if ui_manager.active_panel == 'tasks' and app_state.tasks:
|
||||
new_title = ui_manager.get_user_input("New Task Title: ")
|
||||
selected_task = app_state.tasks[ui_manager.selected_task_idx]
|
||||
app_state.service.rename_task(app_state.active_list_id, selected_task["id"], new_title)
|
||||
app_state.refresh_data() # Refresh display after change
|
||||
elif ui_manager.active_panel == 'lists' and app_state.task_lists:
|
||||
new_title = ui_manager.get_user_input("New List Title: ")
|
||||
if new_title:
|
||||
selected_list = app_state.task_lists[ui_manager.selected_list_idx]
|
||||
app_state.service.rename_list(selected_list['id'], new_title)
|
||||
app_state.refresh_data()
|
||||
|
||||
elif key == ord('a'):
|
||||
if ui_manager.active_panel == 'tasks' and app_state.tasks:
|
||||
new_date = ui_manager.get_user_input("Due Date: ")
|
||||
if is_valid_date(new_date):
|
||||
selected_task = app_state.tasks[ui_manager.selected_task_idx]
|
||||
app_state.service.change_date_task(app_state.active_list_id, selected_task['id'], new_date)
|
||||
app_state.refresh_data()
|
||||
else:
|
||||
ui_manager.show_temporary_message(f"Invalid date format: '{new_date}'")
|
||||
|
||||
|
||||
elif key == ord('i'):
|
||||
if ui_manager.active_panel == 'tasks' and app_state.tasks:
|
||||
open_editor_for_task_notes(stdscr, app_state, ui_manager)
|
||||
|
||||
|
||||
|
||||
elif key == ord('d'):
|
||||
if ui_manager.active_panel == 'tasks' and app_state.tasks:
|
||||
selected_task = app_state.tasks[ui_manager.selected_task_idx]
|
||||
app_state.task_buffer = app_state.service.get_task(app_state.active_list_id, selected_task['id'])
|
||||
app_state.service.delete_task(app_state.active_list_id, selected_task["id"])
|
||||
app_state.refresh_data() # Refresh display after change
|
||||
# Adjust selection after deletion
|
||||
if ui_manager.selected_task_idx >= len(app_state.tasks) and len(app_state.tasks) > 0:
|
||||
ui_manager.selected_task_idx = len(app_state.tasks) - 1
|
||||
elif ui_manager.active_panel == 'lists' and app_state.task_lists:
|
||||
selected_list = app_state.task_lists[ui_manager.selected_list_idx]
|
||||
confirm = ui_manager.get_user_input(f"Delete list '{selected_list['title']}'? (y/n): ")
|
||||
if confirm.lower() == 'y':
|
||||
app_state.list_buffer = selected_list['title']
|
||||
app_state.service.delete_list(selected_list["id"])
|
||||
app_state.task_lists = app_state.service.get_task_lists()
|
||||
if app_state.task_lists:
|
||||
app_state.change_active_list(app_state.task_lists[0]['id'])
|
||||
else:
|
||||
app_state.active_list_id = None
|
||||
app_state.refresh_data()
|
||||
|
||||
elif key == ord('p'):
|
||||
if ui_manager.active_panel == 'tasks':
|
||||
if app_state.tasks:
|
||||
current_task = app_state.tasks[ui_manager.selected_task_idx]
|
||||
unfiltered_tasks = app_state.service.data['tasks'][app_state.active_list_id]
|
||||
unfiltered_index = -1
|
||||
for i, task in enumerate(unfiltered_tasks):
|
||||
if task['id'] == current_task['id']:
|
||||
unfiltered_index = i
|
||||
break
|
||||
|
||||
if unfiltered_index != -1:
|
||||
app_state.service.add_task_body(app_state.active_list_id, app_state.task_buffer, unfiltered_index)
|
||||
else:
|
||||
# Should not happen, but as a fallback, append to the end
|
||||
app_state.service.add_task_body(app_state.active_list_id, app_state.task_buffer)
|
||||
else:
|
||||
# Pasting into an empty list
|
||||
app_state.service.add_task_body(app_state.active_list_id, app_state.task_buffer)
|
||||
app_state.refresh_data()
|
||||
else:
|
||||
app_state.service.add_list(app_state.list_buffer)
|
||||
app_state.refresh_data()
|
||||
|
||||
# Add New Task
|
||||
elif key == ord('o'):
|
||||
if ui_manager.active_panel == 'tasks':
|
||||
new_title = ui_manager.get_user_input("New Task Title: ")
|
||||
if new_title:
|
||||
if app_state.current_parent_task_id:
|
||||
app_state.service.add_task(app_state.active_list_id, new_title, parent=app_state.current_parent_task_id)
|
||||
else:
|
||||
app_state.service.add_task(app_state.active_list_id, new_title)
|
||||
app_state.refresh_data() # Fetch and display the new task
|
||||
else:
|
||||
new_title = ui_manager.get_user_input("New List Title: ")
|
||||
if new_title:
|
||||
app_state.service.add_list(new_title)
|
||||
app_state.refresh_data()
|
||||
|
||||
elif key == ord('?'):
|
||||
ui_manager.toggle_help()
|
||||
|
||||
|
||||
|
||||
return True # Keep the loop running
|
||||
|
||||
def main_loop(stdscr):
|
||||
"""The main application loop function required by curses.wrapper."""
|
||||
# 1. Initialization
|
||||
task_service = TaskService()
|
||||
ui_manager = UIManager(stdscr)
|
||||
app_state = AppState(task_service)
|
||||
|
||||
# Disable cursor visibility for a cleaner TUI
|
||||
curs_set(0)
|
||||
noecho()
|
||||
cbreak()
|
||||
keypad(stdscr, True)
|
||||
|
||||
ui_manager.start_sync_animation()
|
||||
app_state.service.sync_from_google()
|
||||
ui_manager.stop_sync_animation()
|
||||
app_state.refresh_data()
|
||||
|
||||
running = True
|
||||
while running:
|
||||
# 2. Draw the UI based on current state
|
||||
try:
|
||||
parent_task = None
|
||||
if app_state.current_parent_task_id:
|
||||
parent_task = app_state.service.get_task(app_state.active_list_id, app_state.current_parent_task_id)
|
||||
|
||||
parent_ids = app_state.service.get_parent_task_ids(app_state.active_list_id)
|
||||
children_counts = app_state.service.get_children_counts(app_state.active_list_id)
|
||||
|
||||
ui_manager.draw_layout(
|
||||
app_state.task_lists,
|
||||
app_state.tasks,
|
||||
app_state.active_list_id,
|
||||
app_state.task_counts,
|
||||
parent_task=parent_task,
|
||||
parent_ids=parent_ids,
|
||||
children_counts=children_counts
|
||||
)
|
||||
except Exception as e:
|
||||
# Handles window resize errors gracefully
|
||||
ui_manager.show_temporary_message(f"Error: {e}")
|
||||
|
||||
# 3. Handle User Input
|
||||
running = handle_input(stdscr, app_state, ui_manager)
|
||||
|
||||
def cli():
|
||||
try:
|
||||
wrapper(main_loop)
|
||||
except Exception as e:
|
||||
# Print the error before exiting the terminal session
|
||||
print(f"An error occurred: {e}", file=sys.stderr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
Loading…
Reference in new issue