diff --git a/src/tt/controller.py b/src/tt/controller.py index 536575c..67da4c0 100644 --- a/src/tt/controller.py +++ b/src/tt/controller.py @@ -27,24 +27,12 @@ def add_task( def update_task( task_id: int, - text: str | None, - type: str | None, - status: str | None = None, - due: datetime | None = None, + **kwargs, ) -> Task: with db.atomic(): task = Task.get_by_id(task_id) - update_dict = { - "text": text, - "type": type, - "due": due, - "status": status, - } - if status not in [s.value for s in TaskStatus]: - raise ValueError(f"Invalid status: {status}") - - query = Task.update(update_dict).where(Task.id == task_id) + query = Task.update(kwargs).where(Task.id == task_id) query.execute() task = Task.get_by_id(task_id) return task diff --git a/src/tt/db.py b/src/tt/db.py index 987aa41..9cb2131 100644 --- a/src/tt/db.py +++ b/src/tt/db.py @@ -51,6 +51,8 @@ class Task(BaseModel): def initialize_db(): db.connect() db.create_tables([Category, Task]) + if not Category.select().exists(): + Category.create(name="main") db.close() diff --git a/src/tt/tui.py b/src/tt/tui.py new file mode 100644 index 0000000..f4764ec --- /dev/null +++ b/src/tt/tui.py @@ -0,0 +1,193 @@ +from textual.app import App +from textual.widgets import DataTable, Header, Input +from textual.containers import Container +from datetime import datetime + +from .controller import get_tasks, add_task, update_task, TaskStatus +from .db import initialize_db + + +class TaskManagerApp(App): + CSS = """ + Input { + dock: bottom; + margin: 0; + padding: 0; + border: none; + background: $boost; + } + """ + + BINDINGS = [ + # movement + ("h", "cursor_left", "Left"), + ("j", "cursor_down", "Down"), + ("k", "cursor_up", "Up"), + ("l", "cursor_right", "Right"), + ("g", "cursor_top", "Top"), + ("G", "cursor_bottom", "Bottom"), + ("a", "add_task", "Add Task"), + ("c", "start_edit", "Edit Cell"), + ("escape", "cancel_edit", "Cancel Edit"), + ("q", "quit", "Quit"), + ] + + def __init__(self): + super().__init__() + self.edit_row = None + self.edit_column = None + + def compose(self): + yield Header() + yield Container(DataTable()) + # Input is hidden by default + yield Input(id="cell_editor") + + def action_cursor_left(self): + table = self.query_one(DataTable) + table.move_cursor(column=table.cursor_column - 1) + + def action_cursor_right(self): + table = self.query_one(DataTable) + table.move_cursor(column=table.cursor_column + 1) + + def action_cursor_up(self): + table = self.query_one(DataTable) + table.move_cursor(row=table.cursor_row - 1) + + def action_cursor_down(self): + table = self.query_one(DataTable) + y, x = table.cursor_coordinate + table.move_cursor(row=table.cursor_row + 1) + + def action_cursor_top(self): + table = self.query_one(DataTable) + table.move_cursor(row=0) + + def action_cursor_bottom(self): + table = self.query_one(DataTable) + table.move_cursor(row=table.row_count - 1) + + def on_mount(self): + table = self.query_one(DataTable) + table.add_columns("ID", "Task", "Status", "Type", "Category", "Due Date") + self.refresh_tasks() + + def refresh_tasks(self): + """Refresh the tasks table with current data.""" + table = self.query_one(DataTable) + table.clear() + + tasks = get_tasks() + for task in tasks: + # Format the due date nicely + due_str = task.due.strftime("%Y-%m-%d %H:%M") if task.due else "No due date" + + # Get category name or empty string if None + category = task.category.name if task.category else "" + + table.add_row( + str(task.id), + task.text, + task.status, + task.type, + category, + due_str, + key=str(task.id), + ) + + def action_add_task(self): + """Add a new task with default values.""" + add_task( + text="New Task", + type="task", + status=TaskStatus.ZERO.value, + category="main", + ) + self.refresh_tasks() + + def action_start_edit(self): + """Show the input widget for editing the current cell.""" + table = self.query_one(DataTable) + if table.cursor_row is None or table.cursor_column == 0: # Don't edit ID column + return + + # Store the current cell position + self.edit_row = table.cursor_row + self.edit_column = table.cursor_column + + # Get current value and show input + current_value = table.get_cell_at((table.cursor_row, table.cursor_column)) + input_widget = self.query_one("#cell_editor") + input_widget.value = current_value + # input_widget.visible = True + self.set_focus(input_widget) + + def action_cancel_edit(self): + """Hide the input widget without saving.""" + if self.edit_row is not None: + input_widget = self.query_one("#cell_editor") + # input_widget.visible = False + self.edit_row = None + self.edit_column = None + self.set_focus(self.query_one(DataTable)) + + def on_input_submitted(self, event: Input.Submitted): + """Handle the submission of the edit input.""" + if self.edit_row is None: + return + + table = self.query_one(DataTable) + task_id = int(table.get_cell_at((self.edit_row, 0))) + new_value = event.value + + # Map column index to field name + column_to_field = { + 1: "text", + 2: "status", + 3: "type", + 4: "category_name", + 5: "due", + } + + if self.edit_column in column_to_field: + field = column_to_field[self.edit_column] + update_data = {} + + # Special handling for different field types + if field == "due": + try: + update_data["due"] = datetime.strptime(new_value, "%Y-%m-%d") + except ValueError: + self.notify("Invalid date format. Use YYYY-MM-DD") + self.refresh_tasks() + return + elif field == "status": + try: + TaskStatus(new_value) # Validate status + update_data["status"] = new_value + except ValueError: + self.notify(f"Invalid status. Use: {[s.value for s in TaskStatus]}") + self.refresh_tasks() + return + else: + update_data[field] = new_value + + try: + update_task(task_id, **update_data) + self.refresh_tasks() + except Exception as e: + self.notify(f"Error updating task: {str(e)}") + finally: + # Hide the input widget and restore focus + self.action_cancel_edit() + + +def run(): + initialize_db() + app = TaskManagerApp() + app.run() + + +if __name__ == "__main__": + run()