toggling and help

This commit is contained in:
James Turk 2025-01-04 01:08:36 -06:00
parent 5984c28d09
commit 55623a0781
2 changed files with 145 additions and 37 deletions

View File

@ -1,20 +1,35 @@
import re
from textual.app import App
from textual.widgets import DataTable, Header, Input, Static
from textual.screen import ModalScreen
from rich.table import Table
from textual.widgets import (
DataTable,
Header,
Input,
Static,
)
from textual.containers import Container
from datetime import datetime
from .controller import get_tasks, add_task, update_task, TaskStatus
from .db import initialize_db
# TODO: toggle status with 't'
# TODO: add way to filter on other columns
# TODO: safe DB mode
from .utils import (
remove_rich_tag,
advance_enum_val,
get_colored_category,
get_colored_status,
)
# TODO: add filtering on status
# TODO: sorting
# TODO: saved searches
# TODO: date coloring rules (days away, days of week, week of year)
# Nice to Have
# TODO: CLI?
# TODO: CLI
# TODO: config- default category, types, colors
# TODO: dropdowns for status, type, category
# Eventual Goals
# TODO: abstraction
COLUMNS = ("ID", "Task", "Status", "Type", "Due", "Category")
column_to_field = {
1: "text",
2: "status",
@ -24,28 +39,6 @@ column_to_field = {
}
def remove_rich_tag(text):
pattern = r"\[[^\]]*\](.*?)\[/\]"
return re.sub(pattern, r"\1", text)
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 TT(App):
CSS = """
#footer {
@ -102,14 +95,17 @@ class TT(App):
("g", "cursor_top", "Top"),
("G", "cursor_bottom", "Bottom"),
# filtering & editing
("/", "start_search", "Search"),
("f", "start_filter", "Filter"),
("c", "start_edit", "Edit Cell"),
("/", "start_search", "search tasks by name"),
("f", "start_filter", "filter tasks by category"),
("escape", "cancel_edit", "Cancel Edit"),
# edits
("c", "start_edit", "edit current cell"),
("a", "add_task", "add task"),
("t", "toggle_cell", "toggle status"),
("d", "delete_task", "delete (must be on row mode)"),
# other
("a", "add_task", "Add"),
("d", "delete_task", "Delete"),
("q", "quit", "Quit"),
("q", "quit", "quit"),
("?", "show_keys", "show keybindings"),
]
def __init__(self):
@ -117,6 +113,7 @@ class TT(App):
self.search_query = ""
self.search_category = ""
self.saved_cursor_pos = (0, 0)
self.save_on_move = None
def compose(self):
self.header = Header()
@ -138,30 +135,36 @@ class TT(App):
yield self.right_status
def on_mount(self):
self.table.add_columns("ID", "Task", "Status", "Type", "Due", "Category")
self.table.add_columns(*COLUMNS)
self.refresh_tasks(restore_cursor=False)
def action_cursor_left(self):
self.table.move_cursor(column=self.table.cursor_column - 1)
if self.table.cursor_column == 0:
self.table.cursor_type = "row"
self._perform_save_on_move()
def action_cursor_right(self):
self.table.move_cursor(column=self.table.cursor_column + 1)
if self.table.cursor_column != 0:
self.table.cursor_type = "cell"
self._perform_save_on_move()
def action_cursor_up(self):
self.table.move_cursor(row=self.table.cursor_row - 1)
self._perform_save_on_move()
def action_cursor_down(self):
self.table.move_cursor(row=self.table.cursor_row + 1)
self._perform_save_on_move()
def action_cursor_top(self):
self.table.move_cursor(row=0)
self._perform_save_on_move()
def action_cursor_bottom(self):
self.table.move_cursor(row=self.table.row_count - 1)
self._perform_save_on_move()
def refresh_tasks(self, *, restore_cursor=True):
# show table
@ -210,7 +213,6 @@ class TT(App):
self.refresh_tasks()
def action_add_task(self):
"""Add a new task with default values."""
new_task = add_task(
text="New Task",
type="",
@ -221,6 +223,34 @@ class TT(App):
self.move_cursor_to_task(new_task.id)
self.action_start_edit()
def action_toggle_cell(self):
cur_row = self.table.cursor_row
cur_col = self.table.cursor_column
active_col_name = column_to_field[cur_col]
if active_col_name == "status":
task_id = int(self.table.get_cell_at((cur_row, 0)))
current_val = self.table.get_cell_at((cur_row, cur_col))
next_val = advance_enum_val(TaskStatus, current_val)
self.table.update_cell_at((cur_row, cur_col), next_val)
# trigger task_id to be saved on the next cursor move
# this avoids filtered columns disappearing right away
# and tons of DB writes
self._register_save_on_move(task_id, status=next_val)
def _register_save_on_move(self, task_id, **kwargs):
if self.save_on_move and self.save_on_move["task_id"] != task_id:
# we should only ever overwrite the same item
raise Exception("invalid save_on_move state")
self.save_on_move = {"task_id": task_id, **kwargs}
def _perform_save_on_move(self):
if self.save_on_move:
update_task(**self.save_on_move)
self._save_cursor()
self.refresh_tasks()
# reset status
self.save_on_move = None
def move_cursor_to_task(self, task_id):
# ick, but only way to search table?
for row in range(self.table.row_count):
@ -314,6 +344,52 @@ class TT(App):
finally:
self.action_cancel_edit()
def action_show_keys(self):
self.push_screen(KeyBindingsScreen())
class KeyBindingsScreen(ModalScreen):
CSS = """
KeyBindingsScreen {
align: center middle;
}
Vertical {
height: auto;
border: tall $primary;
}
Static.title {
text-align: center;
text-style: bold;
}
Static.footer {
text-align: center;
color: $text-muted;
}
"""
def compose(self):
table = Table(expand=True, show_header=True)
table.add_column("Key")
table.add_column("Description")
table.add_row("j/k/h/l", "up/down/left/right")
table.add_row("g/G", "top/bottom")
table.add_row("esc", "dismiss modal or search bar")
for binding in self.app.BINDINGS:
if binding[0] not in ["h", "j", "k", "l", "g", "G", "escape"]:
table.add_row(binding[0], binding[2])
yield Static("Keybindings", classes="title")
yield Static(table)
yield Static("Press any key to dismiss", classes="footer")
def on_key(self, event) -> None:
self.app.pop_screen()
def run():
initialize_db()

32
src/tt/utils.py Normal file
View File

@ -0,0 +1,32 @@
import re
def remove_rich_tag(text):
"""remove rich styling from a string"""
pattern = r"\[[^\]]*\](.*?)\[/\]"
return re.sub(pattern, r"\1", text)
def advance_enum_val(enum_type, cur_val):
"""advance a value in an enum by one, wrapping around"""
members = [str(e.value) for e in enum_type]
cur_idx = members.index(remove_rich_tag(cur_val))
next_idx = (cur_idx + 1) % len(members)
return members[next_idx]
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}[/]"