diff --git a/src/tt/cli.py b/src/tt/cli.py index 2c534b7..09da82b 100644 --- a/src/tt/cli.py +++ b/src/tt/cli.py @@ -3,7 +3,6 @@ import httpx import lxml.html from typing_extensions import Annotated from .controller.tasks import add_task -from .import_csv import import_tasks_from_csv from .db import initialize_db from .tui.tasks import run as tasks_tui from .tui.overview import run as overview_tui @@ -62,11 +61,5 @@ def overview(): overview_tui() -@app.command() -def import_csv(filename: str): - import_tasks_from_csv(filename) - print("Import complete!") - - if __name__ == "__main__": app() diff --git a/src/tt/config.py b/src/tt/config.py index 5ea50c5..dd7d300 100644 --- a/src/tt/config.py +++ b/src/tt/config.py @@ -17,10 +17,10 @@ def get_enum(name): raise ValueError(f"no such enum! {name}") -def get_view_columns(name): +def get_view(name): for view in _load_config().get("views", []): if view["name"] == name: - return view["columns"] + return view raise ValueError(f"no such view! {name}") diff --git a/src/tt/controller/summaries.py b/src/tt/controller/summaries.py index b8bcbb6..227f371 100644 --- a/src/tt/controller/summaries.py +++ b/src/tt/controller/summaries.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta from peewee import fn, JOIN -from ..db import Task, Category +from ..db import Task def get_category_summary(num: int = 5) -> list[dict]: @@ -13,81 +13,7 @@ def get_category_summary(num: int = 5) -> list[dict]: Returns: List of dicts containing category name and task statistics """ - now = datetime.now() - week_from_now = now + timedelta(days=7) - - # Subquery for overdue tasks - overdue_count = ( - Task.select(Task.category, fn.COUNT(Task.id).alias("overdue")) - .where( - (~Task.deleted) & (Task.due < now) & (Task.status != "done") - ) - .group_by(Task.category) - ) - - # Subquery for tasks due in next 7 days - due_soon_count = ( - Task.select(Task.category, fn.COUNT(Task.id).alias("due_soon")) - .where( - (~Task.deleted) - & (Task.due >= now) - & (Task.due <= week_from_now) - & (Task.status != "done") - ) - .group_by(Task.category) - ) - - # Main query joining all the information - query = ( - Category.select( - Category.name, - fn.COALESCE(fn.SUM(Task.status == "zero"), 0).alias( - "zero_count" - ), - fn.COALESCE(fn.SUM(Task.status == "wip"), 0).alias( - "wip_count" - ), - fn.COALESCE(fn.SUM(Task.status == "blocked"), 0).alias( - "blocked_count" - ), - fn.COALESCE(fn.SUM(Task.status == "done"), 0).alias( - "done_count" - ), - fn.COALESCE(overdue_count.c.overdue, 0).alias("overdue"), - fn.COALESCE(due_soon_count.c.due_soon, 0).alias("due_soon"), - ) - .join(Task, JOIN.LEFT_OUTER) - .join( - overdue_count, - JOIN.LEFT_OUTER, - on=(Category.id == overdue_count.c.category_id), - ) - .join( - due_soon_count, - JOIN.LEFT_OUTER, - on=(Category.id == due_soon_count.c.category_id), - ) - .where(~Task.deleted) - .group_by(Category.id) - .order_by(fn.COUNT(Task.id).desc()) - .limit(num) - ) - - return [ - { - "category": cat.name, - "tasks": { - "zero": cat.zero_count, - "wip": cat.wip_count, - "blocked": cat.blocked_count, - "done": cat.done_count, - "overdue": cat.overdue, - "due_soon": cat.due_soon, - }, - } - for cat in query - ] - + return [] def get_recently_active(num: int = 5, category: str | None = None) -> list[dict]: """ diff --git a/src/tt/controller/tasks.py b/src/tt/controller/tasks.py index e231b85..028f3c4 100644 --- a/src/tt/controller/tasks.py +++ b/src/tt/controller/tasks.py @@ -2,7 +2,7 @@ import json from datetime import datetime from peewee import fn from peewee import Case, Value -from ..db import db, Task, Category, SavedSearch +from ..db import db, Task, SavedSearch from .. import config @@ -84,8 +84,8 @@ def get_tasks( query = query.where(fn.Lower(Task.text).contains(search_text.lower())) if statuses: query = query.where(Task.status.in_(statuses)) - if projects: - query = query.where(Task.project.in_(projects)) + #if projects: + # query = query.where(Task.project.in_(projects)) sort_expressions = _parse_sort_string(sort, statuses) query = query.order_by(*sort_expressions) @@ -93,10 +93,6 @@ def get_tasks( return list(query) -def get_categories() -> list[Category]: - return list(Category.select().order_by(Category.name)) - - def save_view(name: str, *, filters: dict, sort_string: str) -> SavedSearch: filters_json = json.dumps(filters) diff --git a/src/tt/db.py b/src/tt/db.py index 6dd6a21..82cb6e9 100644 --- a/src/tt/db.py +++ b/src/tt/db.py @@ -1,11 +1,9 @@ import json from datetime import date, timedelta, datetime -from enum import Enum from peewee import ( BooleanField, CharField, DateTimeField, - ForeignKeyField, Model, SqliteDatabase, TextField, diff --git a/src/tt/tui/editor.py b/src/tt/tui/editor.py index 8f15d34..07b769e 100644 --- a/src/tt/tui/editor.py +++ b/src/tt/tui/editor.py @@ -24,6 +24,8 @@ class NotifyValidationError(Exception): """will notify and continue if raised""" +ELLIPSIS = "…" + class TableColumnConfig: def __init__( self, @@ -73,7 +75,6 @@ class EnumColumnConfig(TableColumnConfig): def format_for_display(self, val): return get_color_enum(val, self.enum) - def start_change(self, app, current_value): # a weird hack? pass app here and correct modal gets pushed app.push_screen(ChoiceModal(self.enum, current_value), app.apply_change) @@ -93,7 +94,6 @@ class DateColumnConfig(TableColumnConfig): app.push_screen(DateModal(current_value), app.apply_change) -ELLIPSIS = "…" class TableEditor(App): @@ -167,18 +167,25 @@ class TableEditor(App): ("?", "show_keys", "show keybindings"), ] - def __init__(self): + def __init__(self, editor_config: str): super().__init__() - self.filters = {} - self.sort_string = "" # TODO: default sort + self._load_config(editor_config) self.search_query = "" self.saved_cursor_pos = (1, 0) self.save_on_move = None def _load_config(self, name): - # column config - columns = [] - for col in config.get_view_columns(name): + """ + Reads configuration from TOML and sets up: + - table_config + - filters + - sort_string + """ + self.table_config = [] + view = config.get_view(name) + + # set up columns + for col in view["columns"]: field_type = col.get("field_type", "text") field_cls = { "text": TableColumnConfig, @@ -196,8 +203,24 @@ class TableEditor(App): extras["enable_editor"] = True cc = field_cls(field_name, display_name, default=default, **extras) - columns.append(cc) - return columns + self.table_config.append(cc) + + # load default filters + self.filters = view["filters"] + self.sort_string = view["sort"] + + def _db_item_to_row(self, item): + """ + Convert db item to a row for display. + """ + row = [] + + for col in self.table_config: + val = getattr(item, col.field) + display_val = col.format_for_display(val) + row.append(display_val) + + return row def compose(self): self.header = Header() diff --git a/src/tt/tui/modals.py b/src/tt/tui/modals.py index 059a46d..631546f 100644 --- a/src/tt/tui/modals.py +++ b/src/tt/tui/modals.py @@ -45,10 +45,9 @@ class ChoiceModal(ModalScreen): """ BINDINGS = [ - ("j", "cursor_down", "Down"), + ("j,tab", "cursor_down", "Down"), ("k", "cursor_up", "Up"), - Binding("enter", "select", "Select", priority=True), - ("c", "select", "Select"), + Binding("enter", "select", "Select", priority=False), ("escape", "cancel", "cancel"), ] @@ -75,7 +74,8 @@ class ChoiceModal(ModalScreen): def action_select(self): rs = self.query_one(RadioSet) - rs.action_toggle_button() + # TODO: this doesn't work + #rs.action_toggle_button() pressed = rs.pressed_button self.dismiss(str(pressed.label)) @@ -101,9 +101,8 @@ class DateModal(ModalScreen): BINDINGS = [ ("j", "cursor_down", "Down"), ("k", "cursor_up", "Up"), - ("h", "cursor_left", "Left"), + ("h,tab", "cursor_left", "Left"), ("l", "cursor_right", "Right"), - # ("0,1,2,3,4,5,6,7,8,9", "num_entry", "#"), Binding("enter", "select", "Select", priority=True), ("escape", "cancel", "cancel"), ] diff --git a/src/tt/tui/tasks.py b/src/tt/tui/tasks.py index f392d40..e4bf809 100644 --- a/src/tt/tui/tasks.py +++ b/src/tt/tui/tasks.py @@ -21,14 +21,12 @@ class TT(TableEditor): ] def __init__(self, default_view="default"): - super().__init__() - self._load_view(default_view) + super().__init__("tasks") self.update_item_callback = update_task self.add_item_callback = add_task self.get_item_callback = get_task - self.table_config = self._load_config("tasks") - def _load_view(self, name): + def _load_db_view(self, name): try: saved = get_saved_view(name) self.filters = json.loads(saved.filters) @@ -55,19 +53,6 @@ class TT(TableEditor): # if event isn't handled here it will bubble to parent event.prevent_default() - def _db_item_to_row(self, item): - """ - convert db item to an item - """ - row = [] - - for col in self.table_config: - val = getattr(item, col.field) - display_val = col.format_for_display(val) - row.append(display_val) - - return row - def refresh_items(self): items = get_tasks( self.search_query, diff --git a/tt.toml b/tt.toml index f7dc3e8..7ffac1b 100644 --- a/tt.toml +++ b/tt.toml @@ -2,8 +2,8 @@ name = "status" values = [ { value = "zero", color = "#666666" }, - { value = "blocked", color = "#33a99" }, - { value = "wip", color = "#cc9900" }, + { value = "blocked", color = "#cc9900" }, + { value = "wip", color = "#33aa99" }, { value = "done", color = "#009900" }, ] @@ -28,6 +28,10 @@ values = [ [[views]] name = "tasks" +sort = "due" + +[views.filters] +status = "wip,blocked,zero" [[views.columns]] field_name = "id"