Compare commits

...

7 Commits

Author SHA1 Message Date
jpt
a72c6fbaab TagModal 2025-05-04 19:56:35 -05:00
jpt
4f408d7ceb clean up; remove old files 2025-05-04 19:06:12 -05:00
jpt
72588fd9c6 pre-commit 2025-05-04 19:05:29 -05:00
jpt
d24160cfa9 table widths 2025-05-04 18:58:14 -05:00
jpt
98be0fcb2c sync command 2025-05-03 22:36:34 -05:00
jpt
0981bd4d19 sync prototype 2025-05-03 21:26:00 -05:00
jpt
5ca4a54ffb allow selecting view 2025-05-03 18:50:01 -05:00
11 changed files with 193 additions and 136 deletions

24
.pre-commit-config.yaml Normal file
View File

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

View File

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

View File

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

45
src/tt/sync.py Normal file
View File

@ -0,0 +1,45 @@
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_colored_date,
)
from .modals import ChoiceModal, DateModal
from .modals import ChoiceModal, DateModal, TagModal
class NotifyValidationError(Exception):
@ -32,6 +32,7 @@ class TableColumnConfig:
self.read_only = read_only
def preprocess(self, val):
"""from UI -> DB"""
return val # no-op
def start_change(self, app, current_value):
@ -42,6 +43,7 @@ class TableColumnConfig:
app._show_input("edit", current_value)
def format_for_display(self, val):
"""from DB -> UI"""
val = str(val)
if "\n" in val:
val = val.split("\n")[0] + ELLIPSIS
@ -67,6 +69,20 @@ class EnumColumnConfig(TableColumnConfig):
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):
def preprocess(self, val):
try:
@ -88,4 +104,5 @@ def get_col_cls(field_type):
"text": TableColumnConfig,
"enum": EnumColumnConfig,
"date": DateColumnConfig,
"tag": TagColumnConfig,
}[field_type]

View File

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

View File

@ -1,7 +1,7 @@
import datetime
from textual.screen import ModalScreen
from textual.binding import Binding
from textual.widgets import Label
from textual.widgets import Label, Input
from textual.containers import Horizontal, Vertical
from textual.reactive import reactive
from ..utils import get_color_enum
@ -132,6 +132,82 @@ class ChoiceModal(ModalScreen):
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):
CSS = """
DateModal {

View File

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

View File

@ -1,40 +0,0 @@
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()