From daaea24668bf21e28f903627e1143baf79fc4fe1 Mon Sep 17 00:00:00 2001 From: James Turk Date: Fri, 3 Jan 2025 21:32:35 -0600 Subject: [PATCH] working TUI for tasks --- src/tt/tui.py | 226 ++++++++++++++++++++++++++------------------------ 1 file changed, 119 insertions(+), 107 deletions(-) diff --git a/src/tt/tui.py b/src/tt/tui.py index f4764ec..f6f6034 100644 --- a/src/tt/tui.py +++ b/src/tt/tui.py @@ -6,6 +6,31 @@ from datetime import datetime from .controller import get_tasks, add_task, update_task, TaskStatus from .db import initialize_db +column_to_field = { + 1: "text", + 2: "status", + 3: "type", + 4: "due", + 5: "category_name", +} + + +def get_colored_status(status: str) -> str: + colors = { + "zero": "#666666", + "wip": "#33aa99", + "blocked": "#cc9900", + "done": "#009900", + } + return f"[{colors[status]}]{status}[/]" + + +def get_colored_category(category: str) -> str: + hash_val = sum(ord(c) for c in category) + hue = hash_val % 360 + color = f"rgb({hue},200,200) on default" + return f"[{color}]{category}[/]" + class TaskManagerApp(App): CSS = """ @@ -26,73 +51,70 @@ class TaskManagerApp(App): ("l", "cursor_right", "Right"), ("g", "cursor_top", "Top"), ("G", "cursor_bottom", "Bottom"), - ("a", "add_task", "Add Task"), + # filtering & editing + ("/", "start_search", "Search"), ("c", "start_edit", "Edit Cell"), ("escape", "cancel_edit", "Cancel Edit"), + # other + ("a", "add_task", "Add Task"), ("q", "quit", "Quit"), ] def __init__(self): super().__init__() + self.search_query = "" 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) + self.header = Header() + self.table = DataTable() + self.input_bar = Input(id="input_bar") + self.input_bar.display = False + yield self.header + yield Container(self.table) + yield self.input_bar def on_mount(self): - table = self.query_one(DataTable) - table.add_columns("ID", "Task", "Status", "Type", "Category", "Due Date") + self.table.add_columns("ID", "Task", "Status", "Type", "Due", "Category") self.refresh_tasks() + def action_cursor_left(self): + self.table.move_cursor(column=self.table.cursor_column - 1) + + def action_cursor_right(self): + self.table.move_cursor(column=self.table.cursor_column + 1) + + def action_cursor_up(self): + self.table.move_cursor(row=self.table.cursor_row - 1) + + def action_cursor_down(self): + self.table.move_cursor(row=self.table.cursor_row + 1) + + def action_cursor_top(self): + self.table.move_cursor(row=0) + + def action_cursor_bottom(self): + self.table.move_cursor(row=self.table.row_count - 1) + def refresh_tasks(self): - """Refresh the tasks table with current data.""" - table = self.query_one(DataTable) - table.clear() + self.table.clear() - tasks = get_tasks() + tasks = get_tasks(self.search_query) 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" + due_str = task.due.strftime("%Y-%m-%d") if task.due else " - " + category = get_colored_category( + task.category.name if task.category else " - " + ) + status = get_colored_status(task.status) - # Get category name or empty string if None - category = task.category.name if task.category else "" - - table.add_row( + self.table.add_row( str(task.id), task.text, - task.status, + status, task.type, - category, due_str, + category, key=str(task.id), ) @@ -106,81 +128,71 @@ class TaskManagerApp(App): ) 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 + # Control of edit bar #################### - # Store the current cell position - self.edit_row = table.cursor_row - self.edit_column = table.cursor_column + def _show_input(self, mode, start_value): + self.mode = mode + self.input_bar.display = True + self.input_bar.value = start_value + self.set_focus(self.input_bar) - # 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 _hide_input(self): + self.input_bar.display = False + self.set_focus(self.table) 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)) + self._hide_input() - def on_input_submitted(self, event: Input.Submitted): - """Handle the submission of the edit input.""" - if self.edit_row is None: + def action_start_search(self): + self._show_input("search", "") + + def action_start_edit(self): + if self.table.cursor_row is None or self.table.cursor_column == 0: return - table = self.query_one(DataTable) - task_id = int(table.get_cell_at((self.edit_row, 0))) - new_value = event.value + self.saved_cursor_pos = (self.table.cursor_row, self.table.cursor_column) + current_value = self.table.get_cell_at( + (self.table.cursor_row, self.table.cursor_column) + ) + # TODO: remove color codes + self._show_input("edit", current_value) - # Map column index to field name - column_to_field = { - 1: "text", - 2: "status", - 3: "type", - 4: "category_name", - 5: "due", - } + def on_input_submitted(self, event: Input.Submitted): + if self.mode == "search": + self.search_query = event.value + self.refresh_tasks() + elif self.mode == "edit": + self.apply_change(event.value) + self._hide_input() - if self.edit_column in column_to_field: - field = column_to_field[self.edit_column] - update_data = {} + def apply_change(self, new_value): + row, col = self.saved_cursor_pos + task_id = int(self.table.get_cell_at((row, 0))) - # 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 + field = column_to_field[col] + update_data = {} + if field == "due": 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() + update_data["due"] = datetime.strptime(new_value, "%Y-%m-%d") + except ValueError: + self.notify("Invalid date format. Use YYYY-MM-DD") + 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]}") + 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: + self.action_cancel_edit() def run():