commenting/cleanup

This commit is contained in:
jpt 2025-05-12 18:13:24 -05:00
parent b974b2f386
commit 913b56da99
7 changed files with 125 additions and 150 deletions

View File

@ -13,13 +13,17 @@ app = typer.Typer()
@app.command()
def new(
type: str,
name: Annotated[str, typer.Option("-n", "--name", help="name")] = "",
category: Annotated[str, typer.Option("-c", "--category", help="name")] = "",
due: Annotated[str, typer.Option("-d", "--due", help="due")] = "",
url: Annotated[str, typer.Option("-u", "--url", help="URL")] = "",
):
"""
Add new thing without opening TUI.
"""
initialize_db()
required = ["name", "category"]
required = ["name"]
if url and not name:
resp = httpx.get(url, follow_redirects=True)
@ -37,7 +41,7 @@ def new(
# due = typer.prompt("Due (YYYY-MM-DD):")
# TODO: validate/allow blank
add_thing(name, category, due)
add_thing(type=type, text=name, category=category, due=due)
typer.echo("Created new thing!")
@ -45,6 +49,9 @@ def new(
def table(
view: Annotated[str, typer.Option("-v", "--view", help="saved view")] = "tasks",
):
"""
Open default table view.
"""
initialize_db()
things_tui(view)

View File

@ -1,3 +1,10 @@
"""
Actual constants, used minimally.
Most belong in config.py so they can be overriden.
"""
# special purpose for certain dates
SPECIAL_DATES_PIECES = {
"future": (3000, 1, 1),
"unclassified": (1999, 1, 1),

View File

@ -1,3 +1,20 @@
"""
Controller layer for Things.
This layer can rely on `peewee` but no layer above it may.
For TUI Editor, each controller must have functions to:
- get_one(int) -> Obj
- get_many(search_text, filters, sort) -> list[Obj]
- add(**kwargs) -> Obj
- update(id, deleted, **kwargs) -> Obj
(names may vary!)
These will be exposed directly to editor TUI (and web UI).
"""
from peewee import fn
from peewee import Case, Value
from ..db import db, Thing
@ -27,6 +44,10 @@ def update_thing(
type: str | None = None,
**kwargs,
) -> Thing:
"""
Updates a thing in database, treating deleted & type as special params
and everything else goes into the data JSON.
"""
with db.atomic():
thing = Thing.get_by_id(item_id)
updates = {"data": thing.data | kwargs}
@ -80,6 +101,13 @@ def get_things(
filters: dict[str, str] | None = None,
sort: str = "",
) -> list[Thing]:
"""
Main query function.
- search_text: fuzzy match strings against text field
- filters: exact field lookups (will use attribs then data elements)
- sort: comma-separated sort string specifying sort parameters
"""
query = Thing.select().where(~Thing.deleted)
if search_text:

View File

@ -1,3 +1,7 @@
"""
Defines the core data models in peewee ORM
"""
import json
from datetime import date, timedelta, datetime
from xdg_base_dirs import xdg_data_home
@ -11,9 +15,7 @@ from peewee import (
)
from playhouse.sqlite_ext import JSONField
# This module defines the core data types.
#
# Global DB connection
db = SqliteDatabase(
xdg_data_home() / "tt/tt.db",
pragmas={
@ -30,6 +32,26 @@ class BaseModel(Model):
class Thing(BaseModel):
"""
Thing uses sqlite as a sort of schema-less database.
- type corresponds to a table type in config.toml
- data is the JSON data, which adheres (loosely) to the schema defined for the given type
- also contains additional system-wide metadata fields tracked by app
Note: There are a few places in the code where an attribute of Thing is
accessed where the caller can't be sure if a field name is part of the
model or somewhere within data.
Therefore the lookup logic is almost always:
- check for attribute first, e.g. getattr(obj, "field")
- if the attribute does not exist, check for data["field"]
- if neither is present, raise an error
Therefore, `data` cannot contain properties with names used by attributes here.
"""
type = CharField()
data = JSONField()
created_at = DateTimeField(default=datetime.now)
@ -37,15 +59,26 @@ class Thing(BaseModel):
deleted = BooleanField(default=False)
def save(self, *args, **kwargs):
# override of save method to ensure updated_at timestamp is always set
self.updated_at = datetime.now()
# schema enforcement could happen here as well, if/when needed
return super(Thing, self).save(*args, **kwargs)
# @property
# def due_week(self):
# return self.due.isocalendar()[1] - 12
@property
def due_week(self):
# FIXME: this is an experimental hack for spring quarter
# ideally will have a way to define logic about this complex within config
return self.due.isocalendar()[1] - 12
class SavedSearch(BaseModel):
"""
Saved search parameters.
Possibly to be removed in favor of config.toml saved searches?
"""
name = CharField(unique=True)
filters = CharField()
sort_string = CharField()
@ -55,9 +88,14 @@ class SavedSearch(BaseModel):
class ThingGenerator(BaseModel):
"""
Abstract "thing generator" class, allows creation of recurring tasks
and other "things that make other things".
"""
template = CharField()
type = CharField()
config = TextField() # JSON
config = TextField() # TODO: JSON
deleted = BooleanField(default=False)
last_generated_at = DateTimeField(null=True)
created_at = DateTimeField(default=datetime.now)
@ -93,7 +131,7 @@ class ThingGenerator(BaseModel):
if not self.last_generated_at or self.last_generated_at < maybe_next:
return maybe_next
# TODO: this doesn't handle if a month was missed somehow, just advances one
# FIXME: this doesn't handle if a month was missed somehow, just advances one
# same logic as above, if we're stepping another month forward
month += 1
if month == 13:

View File

@ -1,3 +1,7 @@
"""
Experimental sync code.
"""
import httpx
from collections import defaultdict
from .controller.things import get_things

View File

@ -1,136 +0,0 @@
from textual.app import App, ComposeResult
from textual.containers import ScrollableContainer, Horizontal
from textual.widgets import DataTable, Label
from textual.binding import Binding
from datetime import datetime
from ..controller.summaries import (
get_category_summary,
get_recently_active,
get_due_soon,
)
####
# Due Soon # Task Summary
# #
# #
##################################
# WIP # Recently active
# #
# #
# #
class CategoryTable(DataTable):
"""Table showing category summaries"""
def on_mount(self):
self.add_columns(
"Category", "Zero", "WIP", "Blocked", "Done", "Overdue", "Due Soon"
)
self.refresh_data()
def refresh_data(self):
self.clear()
summaries = get_category_summary(10) # Show top 10 categories
for summary in summaries:
self.add_row(
summary["category"],
str(summary["tasks"]["zero"]),
str(summary["tasks"]["wip"]),
str(summary["tasks"]["blocked"]),
str(summary["tasks"]["done"]),
str(summary["tasks"]["overdue"]),
str(summary["tasks"]["due_soon"]),
)
def format_due_date(due_date: datetime | None) -> str:
if not due_date:
return "No due date"
return due_date.strftime("%Y-%m-%d")
class DueTaskList(DataTable):
"""Table showing upcoming and overdue tasks"""
def on_mount(self):
self.add_columns("Task", "Category", "Due Date")
self.refresh_data()
def refresh_data(self):
self.clear()
tasks = get_due_soon(10, all_overdue=True)
for task in tasks:
self.add_row(
task["text"],
task["category"],
format_due_date(task["due"]),
key=str(task["id"]),
)
class RecentTaskList(DataTable):
"""Table showing recently active tasks"""
def on_mount(self):
self.add_columns("Task", "Category", "Due Date")
self.refresh_data()
def refresh_data(self):
self.clear()
tasks = get_recently_active(10)
for task in tasks:
self.add_row(
task["text"],
task["category"] or "-",
format_due_date(task["due"]),
key=str(task["id"]),
)
class Overview(App):
"""Task overview application"""
CSS = """
CategoryTable {
height: 40%;
margin: 1 1;
}
Label {
align: center middle;
background: purple;
}
#lists {
height: 60%;
}
"""
BINDINGS = [
Binding("q", "quit", "Quit"),
Binding("r", "refresh", "Refresh Data"),
]
def compose(self) -> ComposeResult:
with Horizontal(id="top"):
yield CategoryTable()
with Horizontal(id="lists"):
with ScrollableContainer():
yield Label("Due Soon")
yield DueTaskList()
with ScrollableContainer():
yield Label("Activity")
yield RecentTaskList()
def action_refresh(self):
"""Refresh all data in the tables"""
self.query_one(CategoryTable).refresh_data()
self.query_one(DueTaskList).refresh_data()
self.query_one(RecentTaskList).refresh_data()
def run():
app = Overview()
app.run()

View File

@ -1,3 +1,10 @@
"""
General purpose utilities.
Most of these utilities have to do with TUI. Perhaps this becomes tui/utils
once webui is ready.
"""
import re
import os
import datetime
@ -7,6 +14,9 @@ from .constants import SPECIAL_DATES_DISPLAY
def filter_to_string(filters, search_query):
"""
Format/summarize search fields for display.
"""
pieces = []
project = filters.get("project")
status = filters.get("status")
@ -19,14 +29,16 @@ def filter_to_string(filters, search_query):
return "".join(pieces)
def remove_rich_tag(text):
def remove_rich_tag(text: str) -> str:
"""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"""
"""
Advance a value in an enum by one position, wrap around if end is reached.
"""
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)
@ -34,11 +46,20 @@ def advance_enum_val(enum_type, cur_val):
def get_color_enum(value: str, enum: dict[str, dict]) -> str:
"""
Use settings to color enumeration fields in UI.
"""
color = enum.get(value, {"color": "#ff0000"})["color"]
return f"[{color}]{value}[/]"
def get_colored_date(date: datetime.date) -> str:
def get_colored_date(date: datetime.date | str) -> str:
"""
Date coloring function -- heat approach.
TODO: refactor this into a set of rules that can be defined in TOML config
"""
# incoming date can be date or str, we need both
if isinstance(date, datetime.date):
as_date = date
as_str = date.strftime("%Y-%m-%d")
@ -46,7 +67,7 @@ def get_colored_date(date: datetime.date) -> str:
as_date = datetime.datetime.strptime(date, "%Y-%m-%d").date()
as_str = as_date.strftime("%Y-%m-%d")
else:
return "~bad~"
return "~bad~" # so *something* will show up in UI & can be edited
if as_str in SPECIAL_DATES_DISPLAY:
return SPECIAL_DATES_DISPLAY[as_str]
@ -58,6 +79,7 @@ def get_colored_date(date: datetime.date) -> str:
delta = as_date - today
weeks = delta.days // 7
# red,orange,yellow,white gradient
colors = [
# "#FF4000",
"#FF8000",
@ -77,6 +99,11 @@ def get_colored_date(date: datetime.date) -> str:
def get_text_from_editor(initial_text: str = "") -> str | None:
"""
Function that launches $EDITOR with a field's text.
When editor is closed, will read file (if modified) and ingest edited text.
"""
editor = os.environ.get("EDITOR", "vim")
with tempfile.NamedTemporaryFile(suffix=".txt", mode="w+", delete=True) as tf: