toggling and help
This commit is contained in:
parent
5984c28d09
commit
55623a0781
150
src/tt/tui.py
150
src/tt/tui.py
@ -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
32
src/tt/utils.py
Normal 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}[/]"
|
Loading…
Reference in New Issue
Block a user