working TUI for tasks

This commit is contained in:
James Turk 2025-01-03 21:32:35 -06:00
parent e0e8de4e5d
commit daaea24668

View File

@ -6,6 +6,31 @@ from datetime import datetime
from .controller import get_tasks, add_task, update_task, TaskStatus from .controller import get_tasks, add_task, update_task, TaskStatus
from .db import initialize_db 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): class TaskManagerApp(App):
CSS = """ CSS = """
@ -26,73 +51,70 @@ class TaskManagerApp(App):
("l", "cursor_right", "Right"), ("l", "cursor_right", "Right"),
("g", "cursor_top", "Top"), ("g", "cursor_top", "Top"),
("G", "cursor_bottom", "Bottom"), ("G", "cursor_bottom", "Bottom"),
("a", "add_task", "Add Task"), # filtering & editing
("/", "start_search", "Search"),
("c", "start_edit", "Edit Cell"), ("c", "start_edit", "Edit Cell"),
("escape", "cancel_edit", "Cancel Edit"), ("escape", "cancel_edit", "Cancel Edit"),
# other
("a", "add_task", "Add Task"),
("q", "quit", "Quit"), ("q", "quit", "Quit"),
] ]
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.search_query = ""
self.edit_row = None self.edit_row = None
self.edit_column = None self.edit_column = None
def compose(self): def compose(self):
yield Header() self.header = Header()
yield Container(DataTable()) self.table = DataTable()
# Input is hidden by default self.input_bar = Input(id="input_bar")
yield Input(id="cell_editor") self.input_bar.display = False
yield self.header
def action_cursor_left(self): yield Container(self.table)
table = self.query_one(DataTable) yield self.input_bar
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): def on_mount(self):
table = self.query_one(DataTable) self.table.add_columns("ID", "Task", "Status", "Type", "Due", "Category")
table.add_columns("ID", "Task", "Status", "Type", "Category", "Due Date")
self.refresh_tasks() 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): def refresh_tasks(self):
"""Refresh the tasks table with current data.""" self.table.clear()
table = self.query_one(DataTable)
table.clear()
tasks = get_tasks() tasks = get_tasks(self.search_query)
for task in tasks: for task in tasks:
# Format the due date nicely due_str = task.due.strftime("%Y-%m-%d") if task.due else " - "
due_str = task.due.strftime("%Y-%m-%d %H:%M") if task.due else "No due date" 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 self.table.add_row(
category = task.category.name if task.category else ""
table.add_row(
str(task.id), str(task.id),
task.text, task.text,
task.status, status,
task.type, task.type,
category,
due_str, due_str,
category,
key=str(task.id), key=str(task.id),
) )
@ -106,81 +128,71 @@ class TaskManagerApp(App):
) )
self.refresh_tasks() self.refresh_tasks()
def action_start_edit(self): # Control of edit bar ####################
"""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 def _show_input(self, mode, start_value):
self.edit_row = table.cursor_row self.mode = mode
self.edit_column = table.cursor_column self.input_bar.display = True
self.input_bar.value = start_value
self.set_focus(self.input_bar)
# Get current value and show input def _hide_input(self):
current_value = table.get_cell_at((table.cursor_row, table.cursor_column)) self.input_bar.display = False
input_widget = self.query_one("#cell_editor") self.set_focus(self.table)
input_widget.value = current_value
# input_widget.visible = True
self.set_focus(input_widget)
def action_cancel_edit(self): def action_cancel_edit(self):
"""Hide the input widget without saving.""" self._hide_input()
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): def action_start_search(self):
"""Handle the submission of the edit input.""" self._show_input("search", "")
if self.edit_row is None:
def action_start_edit(self):
if self.table.cursor_row is None or self.table.cursor_column == 0:
return return
table = self.query_one(DataTable) self.saved_cursor_pos = (self.table.cursor_row, self.table.cursor_column)
task_id = int(table.get_cell_at((self.edit_row, 0))) current_value = self.table.get_cell_at(
new_value = event.value (self.table.cursor_row, self.table.cursor_column)
)
# TODO: remove color codes
self._show_input("edit", current_value)
# Map column index to field name def on_input_submitted(self, event: Input.Submitted):
column_to_field = { if self.mode == "search":
1: "text", self.search_query = event.value
2: "status", self.refresh_tasks()
3: "type", elif self.mode == "edit":
4: "category_name", self.apply_change(event.value)
5: "due", self._hide_input()
}
if self.edit_column in column_to_field: def apply_change(self, new_value):
field = column_to_field[self.edit_column] row, col = self.saved_cursor_pos
update_data = {} task_id = int(self.table.get_cell_at((row, 0)))
# Special handling for different field types field = column_to_field[col]
if field == "due": update_data = {}
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
if field == "due":
try: try:
update_task(task_id, **update_data) update_data["due"] = datetime.strptime(new_value, "%Y-%m-%d")
self.refresh_tasks() except ValueError:
except Exception as e: self.notify("Invalid date format. Use YYYY-MM-DD")
self.notify(f"Error updating task: {str(e)}") elif field == "status":
finally: try:
# Hide the input widget and restore focus TaskStatus(new_value) # validate status
self.action_cancel_edit() 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(): def run():