Compare commits

..

No commits in common. "a72c6fbaabf484af0600803bb505b29c472f7477" and "6ed62ac3c2bb90e7beece44384d2c83feb5a6be0" have entirely different histories.

11 changed files with 136 additions and 193 deletions

View File

@ -1,24 +0,0 @@
# updated 2025-04-16
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.5
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 # Use the ref you want to point at
hooks:
- id: trailing-whitespace
- id: check-added-large-files
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-json
- id: check-merge-conflict
- id: check-symlinks
- id: check-toml
- id: check-yaml
- id: debug-statements
- id: forbid-submodules
- id: mixed-line-ending
#- id: no-commit-to-branch

View File

@ -3,9 +3,11 @@ import httpx
import lxml.html import lxml.html
import sqlite3 import sqlite3
from typing_extensions import Annotated from typing_extensions import Annotated
from .db import initialize_db, db from .db import initialize_db
from .sync import full_sync
from .tui.things import run as things_tui from .tui.things import run as things_tui
# from .tui.overview import run as overview_tui
from .tui.recurring import run as recurring_tui
from .controller.things import add_thing from .controller.things import add_thing
app = typer.Typer() app = typer.Typer()
@ -43,10 +45,22 @@ def new(
@app.command() @app.command()
def table( def table(
view: Annotated[str, typer.Option("-v", "--view", help="saved view")] = "tasks", view: Annotated[str, typer.Option("-v", "--view", help="saved view")] = "default",
): ):
initialize_db() initialize_db()
things_tui(view) things_tui("tasks")
@app.command()
def generators():
initialize_db()
recurring_tui()
@app.command()
def overview():
initialize_db()
overview_tui()
@app.command() @app.command()
@ -54,6 +68,8 @@ def backup(backup_path: str):
""" """
Perform a SQLite backup using the .backup dot command Perform a SQLite backup using the .backup dot command
""" """
from tt.db import db
conn = db.connection() conn = db.connection()
backup_conn = None backup_conn = None
@ -65,14 +81,5 @@ def backup(backup_path: str):
backup_conn.close() backup_conn.close()
@app.command()
def sync():
"""
Sync with tt server.
"""
initialize_db()
full_sync()
if __name__ == "__main__": if __name__ == "__main__":
app() app()

View File

@ -59,5 +59,3 @@ def get_column(name):
# Valid statuses & projects are read dynamically from the user's config. # Valid statuses & projects are read dynamically from the user's config.
STATUSES = get_enum("status") STATUSES = get_enum("status")
PROJECTS = get_enum("projects") PROJECTS = get_enum("projects")
SERVER_URL = _load_config()["sync"]["url"]
SERVER_KEY = _load_config()["sync"]["key"]

View File

@ -0,0 +1,66 @@
import json
from datetime import date, timedelta
from ..db import db, ThingGenerator
from .things import add_thing
def get_generator(item_id: int) -> ThingGenerator:
return ThingGenerator.get_by_id(item_id)
def get_generators() -> list[ThingGenerator]:
query = ThingGenerator.select().where(~ThingGenerator.deleted)
return query.order_by("type", "template")
def add_generator(
template: str,
type: str,
val: str,
) -> ThingGenerator:
# JSON for future expansion
config = json.dumps({"val": val})
with db.atomic():
thing = ThingGenerator.create(
template=template,
type=type,
config=config,
)
return thing
def generate_needed_things():
to_create = []
for g in get_generators():
next = g.next_at()
if not next:
continue
# TODO: make configurable
if date.today() - next > timedelta(days=14):
to_create.append(
{
"text": g.template.format(next=next),
"project": "recurring",
"due": next,
}
)
for c in to_create:
add_thing(**c)
return to_create
def update_generator(
item_id: int,
**kwargs,
) -> ThingGenerator:
# replace "val" with JSON
if "val" in kwargs:
config = {"val": kwargs.pop("val")}
kwargs["config"] = json.dumps(config)
with db.atomic():
query = ThingGenerator.update(kwargs).where(ThingGenerator.id == item_id)
query.execute()
thing = ThingGenerator.get_by_id(item_id)
return thing

View File

@ -80,8 +80,6 @@ def get_things(
# TODO: which fields are searchable should by dynamic # TODO: which fields are searchable should by dynamic
query = query.where(fn.Lower(Thing.data["text"]).contains(search_text.lower())) query = query.where(fn.Lower(Thing.data["text"]).contains(search_text.lower()))
if filters is None:
filters = {}
for param, val in filters.items(): for param, val in filters.items():
if val is not None: if val is not None:
# no _in query for JSON fields, so use OR # no _in query for JSON fields, so use OR

View File

@ -1,45 +0,0 @@
import httpx
from collections import defaultdict
from .controller.things import get_things
from .config import get_view, get_column, SERVER_KEY, SERVER_URL
def sync_view(view_name):
view = get_view(view_name)
columns = {c: get_column(c) for c in view["columns"]}
filters = view["filters"]
resp = httpx.post(
SERVER_URL + "view/",
params={"api_key": SERVER_KEY},
json={
"name": view["name"],
"columns": columns,
"filters": filters,
"sort": view["sort"],
},
)
resp.raise_for_status()
def sync_thing(thing):
resp = httpx.post(
SERVER_URL + "thing/",
params={"api_key": SERVER_KEY},
json={
"id": thing.id,
"data": thing.data,
"type": thing.type,
},
)
resp.raise_for_status()
return resp.json()
def full_sync():
sync_view("tasks")
actions = defaultdict(int)
# all things for now
for thing in get_things(None, None):
action = sync_thing(thing)["action"]
actions[action] += 1
print(actions)

View File

@ -3,7 +3,7 @@ from ..utils import (
get_color_enum, get_color_enum,
get_colored_date, get_colored_date,
) )
from .modals import ChoiceModal, DateModal, TagModal from .modals import ChoiceModal, DateModal
class NotifyValidationError(Exception): class NotifyValidationError(Exception):
@ -32,7 +32,6 @@ class TableColumnConfig:
self.read_only = read_only self.read_only = read_only
def preprocess(self, val): def preprocess(self, val):
"""from UI -> DB"""
return val # no-op return val # no-op
def start_change(self, app, current_value): def start_change(self, app, current_value):
@ -43,7 +42,6 @@ class TableColumnConfig:
app._show_input("edit", current_value) app._show_input("edit", current_value)
def format_for_display(self, val): def format_for_display(self, val):
"""from DB -> UI"""
val = str(val) val = str(val)
if "\n" in val: if "\n" in val:
val = val.split("\n")[0] + ELLIPSIS val = val.split("\n")[0] + ELLIPSIS
@ -69,20 +67,6 @@ class EnumColumnConfig(TableColumnConfig):
app.push_screen(ChoiceModal(self.enum, current_value), app.apply_change) app.push_screen(ChoiceModal(self.enum, current_value), app.apply_change)
class TagColumnConfig(TableColumnConfig):
def __init__(self, field: str, display_name: str, **kwargs):
super().__init__(field, display_name, **kwargs)
def preprocess(self, val):
return val
def format_for_display(self, val):
return ", ".join(val)
def start_change(self, app, current_value):
app.push_screen(TagModal(current_value), app.apply_change)
class DateColumnConfig(TableColumnConfig): class DateColumnConfig(TableColumnConfig):
def preprocess(self, val): def preprocess(self, val):
try: try:
@ -104,5 +88,4 @@ def get_col_cls(field_type):
"text": TableColumnConfig, "text": TableColumnConfig,
"enum": EnumColumnConfig, "enum": EnumColumnConfig,
"date": DateColumnConfig, "date": DateColumnConfig,
"tag": TagColumnConfig,
}[field_type] }[field_type]

View File

@ -144,7 +144,7 @@ class TableEditor(App):
try: try:
val = getattr(item, col.field) val = getattr(item, col.field)
except AttributeError: except AttributeError:
val = item.data.get(col.field, "~???~") val = item.data[col.field]
display_val = col.format_for_display(val) display_val = col.format_for_display(val)
row.append(display_val) row.append(display_val)
@ -170,10 +170,8 @@ class TableEditor(App):
yield self.right_status yield self.right_status
def on_mount(self): def on_mount(self):
for c in self.table_config: column_names = [c.display_name for c in self.table_config]
self.table.add_column( self.table.add_columns(*column_names)
c.display_name, width=self.view.get("widths", {}).get(c.field, None)
)
self.refresh_data(restore_cursor=False) self.refresh_data(restore_cursor=False)
def action_cursor_left(self): def action_cursor_left(self):
@ -247,9 +245,7 @@ class TableEditor(App):
for fc in self.table_config: for fc in self.table_config:
if fc.read_only: if fc.read_only:
continue continue
# when adding, prefer this order val = self.defaults.get(fc.field, fc.default)
# filter value -> view default -> field config default
val = self.filters.get(fc.field, self.defaults.get(fc.field, fc.default))
if val is not None: if val is not None:
if "," in val: if "," in val:
val = val.split(",")[0] # TODO: fix hack for enums val = val.split(",")[0] # TODO: fix hack for enums

View File

@ -1,7 +1,7 @@
import datetime import datetime
from textual.screen import ModalScreen from textual.screen import ModalScreen
from textual.binding import Binding from textual.binding import Binding
from textual.widgets import Label, Input from textual.widgets import Label
from textual.containers import Horizontal, Vertical from textual.containers import Horizontal, Vertical
from textual.reactive import reactive from textual.reactive import reactive
from ..utils import get_color_enum from ..utils import get_color_enum
@ -132,82 +132,6 @@ class ChoiceModal(ModalScreen):
self.dismiss(self.enum_by_idx[idx]) self.dismiss(self.enum_by_idx[idx])
class TagModal(ModalScreen):
CSS = """
TagModal {
align: center middle;
background: $primary 30%;
}
TagModal Vertical {
border: double teal;
width: 38;
}
TagModal Label.hints {
border: solid grey;
height: 4;
}
TagModal Label {
height: 1;
}
TagEditor #tageditor {
width: 100%;
}
TagModal Label#selected {
background: white;
}
"""
# TODO: float hints to bottom
BINDINGS = [
# ("j,tab", "cursor_down", "Down"),
# ("k,shift+tab", "cursor_up", "Up"),
# TODO: add clear
Binding("enter", "select", "Select", priority=True),
("escape", "cancel", "cancel"),
]
def __init__(self, current_val):
if isinstance(current_val, str):
# FIXME: shouldn't happen
current_val = current_val.split(", ")
self._tags = current_val
self.sel_idx = 0
super().__init__()
def compose(self):
self.input = Input()
with Vertical():
yield self.input
for tag in self._tags:
yield Label(" - " + tag)
yield Label(
"""(h/j/k/l) move
(enter) confirm (esc) quit""",
classes="hints",
)
def action_cursor_down(self):
self._move_cursor(1)
def action_cursor_up(self):
self._move_cursor(-1)
async def action_select(self):
# on first submit: add, second: submit
if tag := self.input.value:
if tag in self._tags:
self._tags.remove(tag)
else:
self._tags.append(self.input.value)
await self.recompose()
self.input.focus()
else:
self.dismiss(self._tags)
def action_cancel(self):
self.app.pop_screen()
class DateModal(ModalScreen): class DateModal(ModalScreen):
CSS = """ CSS = """
DateModal { DateModal {

View File

@ -1,6 +1,6 @@
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.containers import ScrollableContainer, Horizontal from textual.containers import ScrollableContainer, Horizontal
from textual.widgets import DataTable, Label from textual.widgets import DataTable, Static, Label
from textual.binding import Binding from textual.binding import Binding
from datetime import datetime from datetime import datetime
@ -98,11 +98,11 @@ class Overview(App):
margin: 1 1; margin: 1 1;
} }
Label { Label {
align: center middle; align: center middle;
background: purple; background: purple;
} }
#lists { #lists {
height: 60%; height: 60%;
} }

40
src/tt/tui/recurring.py Normal file
View File

@ -0,0 +1,40 @@
import json
from ..controller.generators import (
get_generator,
get_generators,
add_generator,
update_generator,
generate_needed_things,
)
from .editor import (
TableEditor,
)
class GenEditor(TableEditor):
def __init__(self):
super().__init__()
self.update_item_callback = update_generator
self.add_item_callback = add_generator
self.get_item_callback = get_generator
self.table_config = self._load_config("recurring")
def refresh_items(self):
generated = generate_needed_things()
if num := len(generated):
self.notify(f"created {num} things")
items = get_generators()
for item in items:
self.table.add_row(
str(item.id),
item.template,
item.type,
json.loads(item.config)["val"],
str(item.next_at()),
)
def run():
app = GenEditor()
app.run()