Compare commits
2 Commits
85fa719b96
...
55623a0781
Author | SHA1 | Date | |
---|---|---|---|
55623a0781 | |||
5984c28d09 |
@ -10,7 +10,14 @@ from peewee import (
|
|||||||
TextField,
|
TextField,
|
||||||
)
|
)
|
||||||
|
|
||||||
db = SqliteDatabase("tasks.db")
|
db = SqliteDatabase(
|
||||||
|
"tasks.db",
|
||||||
|
pragmas={
|
||||||
|
"journal_mode": "wal",
|
||||||
|
"synchronous": "normal",
|
||||||
|
"foreign_keys": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TaskStatus(Enum):
|
class TaskStatus(Enum):
|
||||||
|
150
src/tt/tui.py
150
src/tt/tui.py
@ -1,20 +1,35 @@
|
|||||||
import re
|
|
||||||
from textual.app import App
|
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 textual.containers import Container
|
||||||
from datetime import datetime
|
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
|
||||||
|
from .utils import (
|
||||||
# TODO: toggle status with 't'
|
remove_rich_tag,
|
||||||
# TODO: add way to filter on other columns
|
advance_enum_val,
|
||||||
# TODO: safe DB mode
|
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
|
# Nice to Have
|
||||||
# TODO: CLI?
|
# TODO: CLI
|
||||||
# TODO: config- default category, types, colors
|
# TODO: config- default category, types, colors
|
||||||
# TODO: dropdowns for status, type, category
|
# TODO: dropdowns for status, type, category
|
||||||
|
# Eventual Goals
|
||||||
|
# TODO: abstraction
|
||||||
|
|
||||||
|
COLUMNS = ("ID", "Task", "Status", "Type", "Due", "Category")
|
||||||
column_to_field = {
|
column_to_field = {
|
||||||
1: "text",
|
1: "text",
|
||||||
2: "status",
|
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):
|
class TT(App):
|
||||||
CSS = """
|
CSS = """
|
||||||
#footer {
|
#footer {
|
||||||
@ -102,14 +95,17 @@ class TT(App):
|
|||||||
("g", "cursor_top", "Top"),
|
("g", "cursor_top", "Top"),
|
||||||
("G", "cursor_bottom", "Bottom"),
|
("G", "cursor_bottom", "Bottom"),
|
||||||
# filtering & editing
|
# filtering & editing
|
||||||
("/", "start_search", "Search"),
|
("/", "start_search", "search tasks by name"),
|
||||||
("f", "start_filter", "Filter"),
|
("f", "start_filter", "filter tasks by category"),
|
||||||
("c", "start_edit", "Edit Cell"),
|
|
||||||
("escape", "cancel_edit", "Cancel Edit"),
|
("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
|
# other
|
||||||
("a", "add_task", "Add"),
|
("q", "quit", "quit"),
|
||||||
("d", "delete_task", "Delete"),
|
("?", "show_keys", "show keybindings"),
|
||||||
("q", "quit", "Quit"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -117,6 +113,7 @@ class TT(App):
|
|||||||
self.search_query = ""
|
self.search_query = ""
|
||||||
self.search_category = ""
|
self.search_category = ""
|
||||||
self.saved_cursor_pos = (0, 0)
|
self.saved_cursor_pos = (0, 0)
|
||||||
|
self.save_on_move = None
|
||||||
|
|
||||||
def compose(self):
|
def compose(self):
|
||||||
self.header = Header()
|
self.header = Header()
|
||||||
@ -138,30 +135,36 @@ class TT(App):
|
|||||||
yield self.right_status
|
yield self.right_status
|
||||||
|
|
||||||
def on_mount(self):
|
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)
|
self.refresh_tasks(restore_cursor=False)
|
||||||
|
|
||||||
def action_cursor_left(self):
|
def action_cursor_left(self):
|
||||||
self.table.move_cursor(column=self.table.cursor_column - 1)
|
self.table.move_cursor(column=self.table.cursor_column - 1)
|
||||||
if self.table.cursor_column == 0:
|
if self.table.cursor_column == 0:
|
||||||
self.table.cursor_type = "row"
|
self.table.cursor_type = "row"
|
||||||
|
self._perform_save_on_move()
|
||||||
|
|
||||||
def action_cursor_right(self):
|
def action_cursor_right(self):
|
||||||
self.table.move_cursor(column=self.table.cursor_column + 1)
|
self.table.move_cursor(column=self.table.cursor_column + 1)
|
||||||
if self.table.cursor_column != 0:
|
if self.table.cursor_column != 0:
|
||||||
self.table.cursor_type = "cell"
|
self.table.cursor_type = "cell"
|
||||||
|
self._perform_save_on_move()
|
||||||
|
|
||||||
def action_cursor_up(self):
|
def action_cursor_up(self):
|
||||||
self.table.move_cursor(row=self.table.cursor_row - 1)
|
self.table.move_cursor(row=self.table.cursor_row - 1)
|
||||||
|
self._perform_save_on_move()
|
||||||
|
|
||||||
def action_cursor_down(self):
|
def action_cursor_down(self):
|
||||||
self.table.move_cursor(row=self.table.cursor_row + 1)
|
self.table.move_cursor(row=self.table.cursor_row + 1)
|
||||||
|
self._perform_save_on_move()
|
||||||
|
|
||||||
def action_cursor_top(self):
|
def action_cursor_top(self):
|
||||||
self.table.move_cursor(row=0)
|
self.table.move_cursor(row=0)
|
||||||
|
self._perform_save_on_move()
|
||||||
|
|
||||||
def action_cursor_bottom(self):
|
def action_cursor_bottom(self):
|
||||||
self.table.move_cursor(row=self.table.row_count - 1)
|
self.table.move_cursor(row=self.table.row_count - 1)
|
||||||
|
self._perform_save_on_move()
|
||||||
|
|
||||||
def refresh_tasks(self, *, restore_cursor=True):
|
def refresh_tasks(self, *, restore_cursor=True):
|
||||||
# show table
|
# show table
|
||||||
@ -210,7 +213,6 @@ class TT(App):
|
|||||||
self.refresh_tasks()
|
self.refresh_tasks()
|
||||||
|
|
||||||
def action_add_task(self):
|
def action_add_task(self):
|
||||||
"""Add a new task with default values."""
|
|
||||||
new_task = add_task(
|
new_task = add_task(
|
||||||
text="New Task",
|
text="New Task",
|
||||||
type="",
|
type="",
|
||||||
@ -221,6 +223,34 @@ class TT(App):
|
|||||||
self.move_cursor_to_task(new_task.id)
|
self.move_cursor_to_task(new_task.id)
|
||||||
self.action_start_edit()
|
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):
|
def move_cursor_to_task(self, task_id):
|
||||||
# ick, but only way to search table?
|
# ick, but only way to search table?
|
||||||
for row in range(self.table.row_count):
|
for row in range(self.table.row_count):
|
||||||
@ -314,6 +344,52 @@ class TT(App):
|
|||||||
finally:
|
finally:
|
||||||
self.action_cancel_edit()
|
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():
|
def run():
|
||||||
initialize_db()
|
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