working TUI for tasks
This commit is contained in:
parent
e0e8de4e5d
commit
daaea24668
226
src/tt/tui.py
226
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():
|
||||
|
Loading…
Reference in New Issue
Block a user