Compare commits
No commits in common. "92cc3c5b40900c97851dd0b7d804687bbe9eb5d2" and "2c5310fe0d36f3eec9e9151ed8c1ed047d585357" have entirely different histories.
92cc3c5b40
...
2c5310fe0d
@ -9,7 +9,6 @@ dependencies = [
|
|||||||
"lxml>=5.3.0",
|
"lxml>=5.3.0",
|
||||||
"peewee>=3.17.8",
|
"peewee>=3.17.8",
|
||||||
"textual>=1.0.0",
|
"textual>=1.0.0",
|
||||||
"tomlkit>=0.13.2",
|
|
||||||
"typer>=0.15.1",
|
"typer>=0.15.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
import tomlkit
|
|
||||||
|
|
||||||
def get_enum(name):
|
|
||||||
with open("tt.toml", "r") as f:
|
|
||||||
config = tomlkit.load(f)
|
|
||||||
|
|
||||||
for enum in config.get("enums", []):
|
|
||||||
if enum["name"] == name:
|
|
||||||
return {v["value"]: v for v in enum["values"]}
|
|
||||||
|
|
||||||
raise ValueError(f"no such enum! {name}")
|
|
||||||
|
|
||||||
STATUSES = get_enum("status")
|
|
||||||
PROJECTS = get_enum("projects")
|
|
@ -1,6 +1,6 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from peewee import fn, JOIN
|
from peewee import fn, JOIN
|
||||||
from ..db import Task, Category
|
from ..db import Task, Category, TaskStatus
|
||||||
|
|
||||||
|
|
||||||
def get_category_summary(num: int = 5) -> list[dict]:
|
def get_category_summary(num: int = 5) -> list[dict]:
|
||||||
@ -20,7 +20,7 @@ def get_category_summary(num: int = 5) -> list[dict]:
|
|||||||
overdue_count = (
|
overdue_count = (
|
||||||
Task.select(Task.category, fn.COUNT(Task.id).alias("overdue"))
|
Task.select(Task.category, fn.COUNT(Task.id).alias("overdue"))
|
||||||
.where(
|
.where(
|
||||||
(~Task.deleted) & (Task.due < now) & (Task.status != "done")
|
(~Task.deleted) & (Task.due < now) & (Task.status != TaskStatus.DONE.value)
|
||||||
)
|
)
|
||||||
.group_by(Task.category)
|
.group_by(Task.category)
|
||||||
)
|
)
|
||||||
@ -32,7 +32,7 @@ def get_category_summary(num: int = 5) -> list[dict]:
|
|||||||
(~Task.deleted)
|
(~Task.deleted)
|
||||||
& (Task.due >= now)
|
& (Task.due >= now)
|
||||||
& (Task.due <= week_from_now)
|
& (Task.due <= week_from_now)
|
||||||
& (Task.status != "done")
|
& (Task.status != TaskStatus.DONE.value)
|
||||||
)
|
)
|
||||||
.group_by(Task.category)
|
.group_by(Task.category)
|
||||||
)
|
)
|
||||||
@ -41,16 +41,16 @@ def get_category_summary(num: int = 5) -> list[dict]:
|
|||||||
query = (
|
query = (
|
||||||
Category.select(
|
Category.select(
|
||||||
Category.name,
|
Category.name,
|
||||||
fn.COALESCE(fn.SUM(Task.status == "zero"), 0).alias(
|
fn.COALESCE(fn.SUM(Task.status == TaskStatus.ZERO.value), 0).alias(
|
||||||
"zero_count"
|
"zero_count"
|
||||||
),
|
),
|
||||||
fn.COALESCE(fn.SUM(Task.status == "wip"), 0).alias(
|
fn.COALESCE(fn.SUM(Task.status == TaskStatus.WIP.value), 0).alias(
|
||||||
"wip_count"
|
"wip_count"
|
||||||
),
|
),
|
||||||
fn.COALESCE(fn.SUM(Task.status == "blocked"), 0).alias(
|
fn.COALESCE(fn.SUM(Task.status == TaskStatus.BLOCKED.value), 0).alias(
|
||||||
"blocked_count"
|
"blocked_count"
|
||||||
),
|
),
|
||||||
fn.COALESCE(fn.SUM(Task.status == "done"), 0).alias(
|
fn.COALESCE(fn.SUM(Task.status == TaskStatus.DONE.value), 0).alias(
|
||||||
"done_count"
|
"done_count"
|
||||||
),
|
),
|
||||||
fn.COALESCE(overdue_count.c.overdue, 0).alias("overdue"),
|
fn.COALESCE(overdue_count.c.overdue, 0).alias("overdue"),
|
||||||
@ -148,7 +148,7 @@ def get_due_soon(
|
|||||||
(~Task.deleted)
|
(~Task.deleted)
|
||||||
& (Task.due.is_null(False))
|
& (Task.due.is_null(False))
|
||||||
& (Task.due != "")
|
& (Task.due != "")
|
||||||
& (Task.status != "done")
|
& (Task.status != TaskStatus.DONE.value)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2,8 +2,7 @@ import json
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from peewee import fn
|
from peewee import fn
|
||||||
from peewee import Case, Value
|
from peewee import Case, Value
|
||||||
from ..db import db, Task, Category, SavedSearch
|
from ..db import db, Task, Category, TaskStatus, SavedSearch
|
||||||
from .. import config
|
|
||||||
|
|
||||||
|
|
||||||
def category_lookup(category):
|
def category_lookup(category):
|
||||||
@ -19,7 +18,7 @@ def get_task(item_id: int) -> Task:
|
|||||||
def add_task(
|
def add_task(
|
||||||
text: str,
|
text: str,
|
||||||
category: str,
|
category: str,
|
||||||
status: str,
|
status: str = TaskStatus.ZERO.value,
|
||||||
due: datetime | None = None,
|
due: datetime | None = None,
|
||||||
type: str = "",
|
type: str = "",
|
||||||
) -> Task:
|
) -> Task:
|
||||||
@ -63,7 +62,7 @@ def _parse_sort_string(sort_string, status_order):
|
|||||||
|
|
||||||
if field == "status":
|
if field == "status":
|
||||||
if not status_order:
|
if not status_order:
|
||||||
status_order = list(config.STATUSES.keys())
|
status_order = [s.value for s in TaskStatus]
|
||||||
# CASE statement that maps each status to its position in the order
|
# CASE statement that maps each status to its position in the order
|
||||||
order_case = Case(
|
order_case = Case(
|
||||||
Task.status,
|
Task.status,
|
||||||
|
14
src/tt/db.py
14
src/tt/db.py
@ -20,6 +20,15 @@ db = SqliteDatabase(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskStatus(Enum):
|
||||||
|
# order is used for progression in toggle
|
||||||
|
ZERO = "zero"
|
||||||
|
WIP = "wip"
|
||||||
|
BLOCKED = "blocked"
|
||||||
|
DONE = "done"
|
||||||
|
|
||||||
|
|
||||||
class GeneratorType(Enum):
|
class GeneratorType(Enum):
|
||||||
DAYS_BETWEEN = "days-btwn"
|
DAYS_BETWEEN = "days-btwn"
|
||||||
MONTHLY = "monthly"
|
MONTHLY = "monthly"
|
||||||
@ -39,7 +48,10 @@ class Category(BaseModel):
|
|||||||
|
|
||||||
class Task(BaseModel):
|
class Task(BaseModel):
|
||||||
text = TextField()
|
text = TextField()
|
||||||
status = CharField()
|
status = CharField(
|
||||||
|
choices=[(status.value, status.name) for status in TaskStatus],
|
||||||
|
default=TaskStatus.ZERO.value,
|
||||||
|
)
|
||||||
due = DateTimeField(null=True)
|
due = DateTimeField(null=True)
|
||||||
category = ForeignKeyField(Category, backref="tasks", null=True)
|
category = ForeignKeyField(Category, backref="tasks", null=True)
|
||||||
type = CharField()
|
type = CharField()
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import csv
|
import csv
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from tt.db import initialize_db, Task, Category
|
from tt.db import initialize_db, Task, Category, TaskStatus
|
||||||
from tt.config import STATUSES
|
|
||||||
|
|
||||||
|
|
||||||
def import_tasks_from_csv(filename: str):
|
def import_tasks_from_csv(filename: str):
|
||||||
@ -21,7 +20,9 @@ def import_tasks_from_csv(filename: str):
|
|||||||
|
|
||||||
# Validate status
|
# Validate status
|
||||||
status = row["status"].lower() if row["status"] else "zero"
|
status = row["status"].lower() if row["status"] else "zero"
|
||||||
if status not in STATUSES:
|
try:
|
||||||
|
TaskStatus(status)
|
||||||
|
except ValueError:
|
||||||
print(f"Warning: Invalid status '{status}', defaulting to 'zero'")
|
print(f"Warning: Invalid status '{status}', defaulting to 'zero'")
|
||||||
status = "zero"
|
status = "zero"
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import datetime
|
|
||||||
from textual.app import App
|
from textual.app import App
|
||||||
from textual.widgets import (
|
from textual.widgets import (
|
||||||
DataTable,
|
DataTable,
|
||||||
@ -11,15 +10,30 @@ from textual.containers import Container
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
remove_rich_tag,
|
remove_rich_tag,
|
||||||
filter_to_string,
|
filter_to_string,
|
||||||
|
advance_enum_val,
|
||||||
get_text_from_editor,
|
get_text_from_editor,
|
||||||
)
|
)
|
||||||
from .keymodal import KeyModal
|
from .keymodal import KeyModal
|
||||||
from .modals import ChoiceModal, DateModal, ConfirmModal
|
|
||||||
|
ELLIPSIS = "…"
|
||||||
|
|
||||||
|
|
||||||
class NotifyValidationError(Exception):
|
class NotifyValidationError(Exception):
|
||||||
"""will notify and continue if raised"""
|
"""will notify and continue if raised"""
|
||||||
|
|
||||||
|
|
||||||
|
def _enum_preprocessor(enumCls):
|
||||||
|
"""generate a default preprocessor to enforce enums"""
|
||||||
|
|
||||||
|
def preprocessor(val):
|
||||||
|
try:
|
||||||
|
enumCls(val)
|
||||||
|
return val
|
||||||
|
except ValueError:
|
||||||
|
raise NotifyValidationError(
|
||||||
|
f"Invalid value. Use: {[s.value for s in enumCls]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TableColumnConfig:
|
class TableColumnConfig:
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -28,6 +42,8 @@ class TableColumnConfig:
|
|||||||
display_name: str,
|
display_name: str,
|
||||||
*,
|
*,
|
||||||
default=None,
|
default=None,
|
||||||
|
enum=None,
|
||||||
|
preprocessor=None,
|
||||||
enable_editor=False,
|
enable_editor=False,
|
||||||
filterable=True,
|
filterable=True,
|
||||||
read_only=False,
|
read_only=False,
|
||||||
@ -35,57 +51,16 @@ class TableColumnConfig:
|
|||||||
self.field = field
|
self.field = field
|
||||||
self.display_name = display_name
|
self.display_name = display_name
|
||||||
self.default = default
|
self.default = default
|
||||||
|
self.enum = enum
|
||||||
self.enable_editor = enable_editor
|
self.enable_editor = enable_editor
|
||||||
self.filterable = filterable
|
self.filterable = filterable
|
||||||
self.read_only = read_only
|
self.read_only = read_only
|
||||||
|
if preprocessor:
|
||||||
def preprocess(self, val):
|
self.preprocessor = preprocessor
|
||||||
return val # no-op
|
elif self.enum:
|
||||||
|
self.preprocessor = _enum_preprocessor(self.enum)
|
||||||
def start_change(self, app, current_value):
|
|
||||||
if current_value.endswith(ELLIPSIS):
|
|
||||||
app.action_start_edit()
|
|
||||||
else:
|
else:
|
||||||
# default edit mode
|
self.preprocessor = lambda x: x
|
||||||
app._show_input("edit", current_value)
|
|
||||||
|
|
||||||
|
|
||||||
class EnumColumnConfig(TableColumnConfig):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
field: str,
|
|
||||||
display_name: str,
|
|
||||||
enum,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
super().__init__(field, display_name, **kwargs)
|
|
||||||
self.enum = enum
|
|
||||||
|
|
||||||
def preprocess(self, val):
|
|
||||||
if val in self.enum:
|
|
||||||
return val
|
|
||||||
else:
|
|
||||||
raise NotifyValidationError(
|
|
||||||
f"Invalid value {val}. Use: {list(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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DateColumnConfig(TableColumnConfig):
|
|
||||||
def preprocess(self, val):
|
|
||||||
try:
|
|
||||||
return datetime.strptime(val, "%Y-%m-%d")
|
|
||||||
except ValueError:
|
|
||||||
raise NotifyValidationError("Invalid date format. Use YYYY-MM-DD")
|
|
||||||
|
|
||||||
def start_change(self, app, current_value):
|
|
||||||
app.push_screen(DateModal(current_value), app.apply_change)
|
|
||||||
ELLIPSIS = "…"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TableEditor(App):
|
class TableEditor(App):
|
||||||
@ -159,7 +134,7 @@ class TableEditor(App):
|
|||||||
("?", "show_keys", "show keybindings"),
|
("?", "show_keys", "show keybindings"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, default_view="default"):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.filters = {}
|
self.filters = {}
|
||||||
self.sort_string = "" # TODO: default sort
|
self.sort_string = "" # TODO: default sort
|
||||||
@ -237,10 +212,7 @@ class TableEditor(App):
|
|||||||
self.table.move_cursor(row=0, column=1)
|
self.table.move_cursor(row=0, column=1)
|
||||||
|
|
||||||
def action_delete_item(self):
|
def action_delete_item(self):
|
||||||
self.push_screen(ConfirmModal(f"delete ?"), self._delete_item_callback)
|
if self.table.cursor_column == 0:
|
||||||
|
|
||||||
def _delete_item_callback(self, confirm):
|
|
||||||
if confirm and self.table.cursor_column == 0:
|
|
||||||
cur_row = self.table.cursor_row
|
cur_row = self.table.cursor_row
|
||||||
item_id = int(self.table.get_cell_at((cur_row, 0)))
|
item_id = int(self.table.get_cell_at((cur_row, 0)))
|
||||||
# deletable items need a delete
|
# deletable items need a delete
|
||||||
@ -257,9 +229,11 @@ class TableEditor(App):
|
|||||||
continue
|
continue
|
||||||
val = self.filters.get(fc.field, fc.default)
|
val = self.filters.get(fc.field, fc.default)
|
||||||
if val is not None:
|
if val is not None:
|
||||||
if "," in val:
|
# enums use comma separated filters
|
||||||
val = val.split(",")[0] # TODO: fix hack for enums
|
if fc.enum:
|
||||||
prepopulated[fc.field] = val
|
prepopulated[fc.field] = val.split(",")[0]
|
||||||
|
else:
|
||||||
|
prepopulated[fc.field] = val
|
||||||
|
|
||||||
new_item = self.add_item_callback(**prepopulated)
|
new_item = self.add_item_callback(**prepopulated)
|
||||||
self.refresh_data(restore_cursor=False)
|
self.refresh_data(restore_cursor=False)
|
||||||
@ -273,21 +247,21 @@ class TableEditor(App):
|
|||||||
def _active_item_id(self):
|
def _active_item_id(self):
|
||||||
return int(self.table.get_cell_at((self.table.cursor_row, 0)))
|
return int(self.table.get_cell_at((self.table.cursor_row, 0)))
|
||||||
|
|
||||||
# def action_toggle_cell(self):
|
def action_toggle_cell(self):
|
||||||
# cur_row = self.table.cursor_row
|
cur_row = self.table.cursor_row
|
||||||
# cur_col = self.table.cursor_column
|
cur_col = self.table.cursor_column
|
||||||
# cconf = self._active_column_config()
|
cconf = self._active_column_config()
|
||||||
|
|
||||||
# if cconf.enum:
|
if cconf.enum:
|
||||||
# item_id = self._active_item_id()
|
item_id = self._active_item_id()
|
||||||
# current_val = self.table.get_cell_at((cur_row, cur_col))
|
current_val = self.table.get_cell_at((cur_row, cur_col))
|
||||||
# next_val = advance_enum_val(cconf.enum, current_val)
|
next_val = advance_enum_val(cconf.enum, current_val)
|
||||||
# self.table.update_cell_at((cur_row, cur_col), next_val)
|
self.table.update_cell_at((cur_row, cur_col), next_val)
|
||||||
# # trigger item_id to be saved on the next cursor move
|
# trigger item_id to be saved on the next cursor move
|
||||||
# # this avoids filtered columns disappearing right away
|
# this avoids filtered columns disappearing right away
|
||||||
# # and tons of DB writes
|
# and tons of DB writes
|
||||||
# update = {cconf.field: next_val}
|
update = {cconf.field: next_val}
|
||||||
# self._register_save_on_move(item_id, **update)
|
self._register_save_on_move(item_id, **update)
|
||||||
|
|
||||||
def _register_save_on_move(self, item_id, **kwargs):
|
def _register_save_on_move(self, item_id, **kwargs):
|
||||||
if self.save_on_move and self.save_on_move["item_id"] != item_id:
|
if self.save_on_move and self.save_on_move["item_id"] != item_id:
|
||||||
@ -359,15 +333,15 @@ class TableEditor(App):
|
|||||||
if cconf.read_only:
|
if cconf.read_only:
|
||||||
return
|
return
|
||||||
|
|
||||||
# save cursor before callback, so correct position updates
|
|
||||||
self._save_cursor()
|
self._save_cursor()
|
||||||
current_value = self.table.get_cell_at(
|
current_value = self.table.get_cell_at(
|
||||||
(self.table.cursor_row, self.table.cursor_column)
|
(self.table.cursor_row, self.table.cursor_column)
|
||||||
)
|
)
|
||||||
|
if current_value.endswith(ELLIPSIS):
|
||||||
|
self.notify("multi-line text, use (e)dit")
|
||||||
|
return
|
||||||
current_value = remove_rich_tag(current_value)
|
current_value = remove_rich_tag(current_value)
|
||||||
|
self._show_input("edit", current_value)
|
||||||
# delegate to start_change, which will call back to self
|
|
||||||
cconf.start_change(self, current_value)
|
|
||||||
|
|
||||||
def action_start_edit(self):
|
def action_start_edit(self):
|
||||||
cconf = self._active_column_config()
|
cconf = self._active_column_config()
|
||||||
@ -418,7 +392,7 @@ class TableEditor(App):
|
|||||||
|
|
||||||
# preprocess/validate the field being saved
|
# preprocess/validate the field being saved
|
||||||
try:
|
try:
|
||||||
update_data[field] = cconf.preprocess(new_value)
|
update_data[field] = cconf.preprocessor(new_value)
|
||||||
self.update_item_callback(item_id, **update_data)
|
self.update_item_callback(item_id, **update_data)
|
||||||
self.refresh_data()
|
self.refresh_data()
|
||||||
except NotifyValidationError as e:
|
except NotifyValidationError as e:
|
||||||
|
@ -1,186 +0,0 @@
|
|||||||
from textual.screen import ModalScreen
|
|
||||||
from textual.binding import Binding
|
|
||||||
from textual.widgets import RadioSet, RadioButton, Label
|
|
||||||
from .. import config
|
|
||||||
from ..utils import get_color_enum
|
|
||||||
|
|
||||||
class ConfirmModal(ModalScreen):
|
|
||||||
CSS = """
|
|
||||||
ConfirmModal {
|
|
||||||
align: center middle;
|
|
||||||
background: $primary 30%;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
BINDINGS = [
|
|
||||||
("y", "confirm", "Down"),
|
|
||||||
("n,escape", "cancel", "cancel"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, message):
|
|
||||||
self.message = message
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def compose(self):
|
|
||||||
yield Label(self.message)
|
|
||||||
yield Label("(y)es")
|
|
||||||
yield Label("(n)o")
|
|
||||||
|
|
||||||
def action_confirm(self):
|
|
||||||
self.dismiss(True)
|
|
||||||
|
|
||||||
def action_cancel(self):
|
|
||||||
self.app.pop_screen()
|
|
||||||
|
|
||||||
|
|
||||||
class ChoiceModal(ModalScreen):
|
|
||||||
CSS = """
|
|
||||||
ChoiceModal {
|
|
||||||
align: center middle;
|
|
||||||
background: $primary 30%;
|
|
||||||
}
|
|
||||||
ChoiceModal Label {
|
|
||||||
height: 1;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
BINDINGS = [
|
|
||||||
("j", "cursor_down", "Down"),
|
|
||||||
("k", "cursor_up", "Up"),
|
|
||||||
Binding("enter", "select", "Select", priority=True),
|
|
||||||
("c", "select", "Select"),
|
|
||||||
("escape", "cancel", "cancel"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, enum, selected):
|
|
||||||
self._enum = enum
|
|
||||||
self.selected = selected
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def compose(self):
|
|
||||||
yield RadioSet(
|
|
||||||
*[
|
|
||||||
RadioButton(
|
|
||||||
get_color_enum(e.value, config.STATUSES, "red"), value=self.selected == str(e.value)
|
|
||||||
)
|
|
||||||
for e in self._enum
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def action_cursor_down(self):
|
|
||||||
self.query_one(RadioSet).action_next_button()
|
|
||||||
|
|
||||||
def action_cursor_up(self):
|
|
||||||
self.query_one(RadioSet).action_previous_button()
|
|
||||||
|
|
||||||
def action_select(self):
|
|
||||||
rs = self.query_one(RadioSet)
|
|
||||||
rs.action_toggle_button()
|
|
||||||
pressed = rs.pressed_button
|
|
||||||
self.dismiss(str(pressed.label))
|
|
||||||
|
|
||||||
def action_cancel(self):
|
|
||||||
self.app.pop_screen()
|
|
||||||
|
|
||||||
|
|
||||||
class DateModal(ModalScreen):
|
|
||||||
CSS = """
|
|
||||||
DateModal {
|
|
||||||
layout: horizontal;
|
|
||||||
align: center middle;
|
|
||||||
background: $primary 30%;
|
|
||||||
}
|
|
||||||
DateModal Label {
|
|
||||||
border: solid grey;
|
|
||||||
}
|
|
||||||
DateModal Label.selected-date {
|
|
||||||
border: solid green;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
BINDINGS = [
|
|
||||||
("j", "cursor_down", "Down"),
|
|
||||||
("k", "cursor_up", "Up"),
|
|
||||||
("h", "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"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, date):
|
|
||||||
self.pieces = [int(p) for p in date.split("-")]
|
|
||||||
self.selected = 1 # start on month
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def compose(self):
|
|
||||||
for idx, piece in enumerate(self.pieces):
|
|
||||||
yield Label(
|
|
||||||
str(piece), classes="selected-date" if idx == self.selected else ""
|
|
||||||
)
|
|
||||||
|
|
||||||
def action_cursor_left(self):
|
|
||||||
# cycle Y/M/D
|
|
||||||
self.selected = (self.selected - 1) % 3
|
|
||||||
self._update_highlight()
|
|
||||||
|
|
||||||
def action_cursor_right(self):
|
|
||||||
self.selected = (self.selected + 1) % 3
|
|
||||||
self._update_highlight()
|
|
||||||
|
|
||||||
def _update_highlight(self):
|
|
||||||
for idx, lbl in enumerate(self.query("Label")):
|
|
||||||
if idx == self.selected:
|
|
||||||
lbl.add_class("selected-date")
|
|
||||||
else:
|
|
||||||
lbl.remove_class("selected-date")
|
|
||||||
|
|
||||||
def max_for(self, piece):
|
|
||||||
days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
||||||
if piece == 0:
|
|
||||||
return 3000
|
|
||||||
elif piece == 1:
|
|
||||||
return 12
|
|
||||||
else:
|
|
||||||
# -1 for offset array
|
|
||||||
return days_in_month[self.pieces[1] - 1]
|
|
||||||
|
|
||||||
def _move_piece(self, by):
|
|
||||||
cur_value = self.pieces[self.selected]
|
|
||||||
cur_value += by
|
|
||||||
if cur_value == 0:
|
|
||||||
cur_value = self.max_for(self.selected)
|
|
||||||
if cur_value > self.max_for(self.selected):
|
|
||||||
cur_value = 1
|
|
||||||
self.pieces[self.selected] = cur_value
|
|
||||||
cur_label = self.query("Label")[self.selected]
|
|
||||||
cur_label.update(str(cur_value))
|
|
||||||
|
|
||||||
def action_cursor_down(self):
|
|
||||||
self._move_piece(-1)
|
|
||||||
|
|
||||||
def action_cursor_up(self):
|
|
||||||
self._move_piece(1)
|
|
||||||
|
|
||||||
def on_key(self, event) -> None:
|
|
||||||
key = event.key
|
|
||||||
if key in "0123456789":
|
|
||||||
cur_value = self.pieces[self.selected]
|
|
||||||
# If we can append the values like 1+2 = 12
|
|
||||||
# then do that, otherwise reset the value
|
|
||||||
# so (for months) 1+3 = 3
|
|
||||||
appended = int(str(cur_value) + key)
|
|
||||||
if appended <= self.max_for(self.selected):
|
|
||||||
cur_value = appended
|
|
||||||
else:
|
|
||||||
cur_value = int(key)
|
|
||||||
self.pieces[self.selected] = cur_value
|
|
||||||
self._move_piece(0)
|
|
||||||
event.prevent_default()
|
|
||||||
|
|
||||||
def action_select(self):
|
|
||||||
date = "-".join(str(p) for p in self.pieces)
|
|
||||||
self.dismiss(date)
|
|
||||||
|
|
||||||
def action_cancel(self):
|
|
||||||
self.app.pop_screen()
|
|
@ -11,7 +11,6 @@ from ..db import GeneratorType
|
|||||||
from .editor import (
|
from .editor import (
|
||||||
TableEditor,
|
TableEditor,
|
||||||
TableColumnConfig,
|
TableColumnConfig,
|
||||||
EnumColumnConfig,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -19,14 +18,14 @@ class TaskGenEditor(TableEditor):
|
|||||||
TABLE_CONFIG = (
|
TABLE_CONFIG = (
|
||||||
TableColumnConfig("id", "ID"),
|
TableColumnConfig("id", "ID"),
|
||||||
TableColumnConfig("template", "Template", default="recur {val}"),
|
TableColumnConfig("template", "Template", default="recur {val}"),
|
||||||
EnumColumnConfig(
|
TableColumnConfig(
|
||||||
"type",
|
"type",
|
||||||
"Type",
|
"Type",
|
||||||
default=GeneratorType.DAYS_BETWEEN.value,
|
default=GeneratorType.DAYS_BETWEEN.value,
|
||||||
enum=GeneratorType,
|
enum=GeneratorType,
|
||||||
),
|
),
|
||||||
TableColumnConfig("val", "Value", default="1"),
|
TableColumnConfig("val", "Value", default="1"),
|
||||||
TableColumnConfig("next_at", "Next @", read_only=True),
|
TableColumnConfig("next_at", "Next @", default="", read_only=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -2,40 +2,47 @@ import json
|
|||||||
from textual.widgets import Input
|
from textual.widgets import Input
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from .. import config
|
|
||||||
from ..controller.tasks import (
|
from ..controller.tasks import (
|
||||||
get_task,
|
get_task,
|
||||||
get_tasks,
|
get_tasks,
|
||||||
add_task,
|
add_task,
|
||||||
update_task,
|
update_task,
|
||||||
|
TaskStatus,
|
||||||
save_view,
|
save_view,
|
||||||
get_saved_view,
|
get_saved_view,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
get_color_enum,
|
get_colored_category,
|
||||||
|
get_colored_status,
|
||||||
get_colored_date,
|
get_colored_date,
|
||||||
)
|
)
|
||||||
from .editor import (
|
from .editor import (
|
||||||
TableEditor,
|
TableEditor,
|
||||||
TableColumnConfig,
|
TableColumnConfig,
|
||||||
EnumColumnConfig,
|
NotifyValidationError,
|
||||||
DateColumnConfig,
|
|
||||||
ELLIPSIS,
|
ELLIPSIS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def due_preprocessor(val):
|
||||||
|
try:
|
||||||
|
return datetime.strptime(val, "%Y-%m-%d")
|
||||||
|
except ValueError:
|
||||||
|
raise NotifyValidationError("Invalid date format. Use YYYY-MM-DD")
|
||||||
|
|
||||||
|
|
||||||
class TT(TableEditor):
|
class TT(TableEditor):
|
||||||
TABLE_CONFIG = (
|
TABLE_CONFIG = (
|
||||||
TableColumnConfig("id", "ID"),
|
TableColumnConfig("id", "ID"),
|
||||||
TableColumnConfig("text", "Task", default="new task", enable_editor=True),
|
TableColumnConfig("text", "Task", default="new task", enable_editor=True),
|
||||||
EnumColumnConfig(
|
TableColumnConfig(
|
||||||
"status",
|
"status",
|
||||||
"Status",
|
"Status",
|
||||||
enum=config.STATUSES,
|
|
||||||
default="zero",
|
default="zero",
|
||||||
|
enum=TaskStatus,
|
||||||
),
|
),
|
||||||
TableColumnConfig("type", "Type", default=""),
|
TableColumnConfig("type", "Type", default=""),
|
||||||
DateColumnConfig("due", "Due", default=""),
|
TableColumnConfig("due", "Due", default="", preprocessor=due_preprocessor),
|
||||||
TableColumnConfig("category", "Category", default="main"),
|
TableColumnConfig("category", "Category", default="main"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -89,12 +96,10 @@ class TT(TableEditor):
|
|||||||
sort=self.sort_string,
|
sort=self.sort_string,
|
||||||
)
|
)
|
||||||
for item in items:
|
for item in items:
|
||||||
category = get_color_enum(
|
category = get_colored_category(
|
||||||
item.category.name if item.category else " - ",
|
item.category.name if item.category else " - "
|
||||||
config.PROJECTS,
|
|
||||||
"grey"
|
|
||||||
)
|
)
|
||||||
status = get_color_enum(item.status, config.STATUSES, "red")
|
status = get_colored_status(item.status)
|
||||||
due = get_colored_date(item.due)
|
due = get_colored_date(item.due)
|
||||||
|
|
||||||
if "\n" in item.text:
|
if "\n" in item.text:
|
||||||
|
@ -32,9 +32,15 @@ def advance_enum_val(enum_type, cur_val):
|
|||||||
return members[next_idx]
|
return members[next_idx]
|
||||||
|
|
||||||
|
|
||||||
def get_color_enum(value: str, enum: dict[str, dict], default: str) -> str:
|
def get_colored_status(status: str) -> str:
|
||||||
color = enum.get(value, {"color": default})["color"]
|
colors = {
|
||||||
return f"[{color}]{value}[/]"
|
"zero": "#666666",
|
||||||
|
"wip": "#33aa99",
|
||||||
|
"blocked": "#cc9900",
|
||||||
|
"done": "#009900",
|
||||||
|
}
|
||||||
|
color = colors.get(status, "#666666")
|
||||||
|
return f"[{color}]{status}[/]"
|
||||||
|
|
||||||
|
|
||||||
def get_colored_category(category: str) -> str:
|
def get_colored_category(category: str) -> str:
|
||||||
|
21
tt.toml
21
tt.toml
@ -1,21 +0,0 @@
|
|||||||
[[enums]]
|
|
||||||
name = "status"
|
|
||||||
values = [
|
|
||||||
{ value = "zero", color = "#666666" },
|
|
||||||
{ value = "blocked", color = "#33a99" },
|
|
||||||
{ value = "wip", color = "#cc9900" },
|
|
||||||
{ value = "done", color = "#009900" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[enums]]
|
|
||||||
name = "projects"
|
|
||||||
values = [
|
|
||||||
{ value = "SECT", color = "purple" },
|
|
||||||
{ value = "life", color = "#00cc00" },
|
|
||||||
{ value = "CAPP", color = "#cc0000" },
|
|
||||||
{ value = "ilikethis", color = "#cccc00" },
|
|
||||||
{ value = "krang", color = "#ff00ff"},
|
|
||||||
{ value = "artworld", color = "#0000cc"},
|
|
||||||
{ value = "TT", color = "#00ff00"},
|
|
||||||
]
|
|
||||||
|
|
14
uv.lock
generated
14
uv.lock
generated
@ -1,5 +1,4 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 1
|
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -161,7 +160,7 @@ name = "click"
|
|||||||
version = "8.1.8"
|
version = "8.1.8"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
{ name = "colorama", marker = "platform_system == 'Windows'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
|
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
|
||||||
wheels = [
|
wheels = [
|
||||||
@ -902,15 +901,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
|
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tomlkit"
|
|
||||||
version = "0.13.2"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tt"
|
name = "tt"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -920,7 +910,6 @@ dependencies = [
|
|||||||
{ name = "lxml" },
|
{ name = "lxml" },
|
||||||
{ name = "peewee" },
|
{ name = "peewee" },
|
||||||
{ name = "textual" },
|
{ name = "textual" },
|
||||||
{ name = "tomlkit" },
|
|
||||||
{ name = "typer" },
|
{ name = "typer" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -937,7 +926,6 @@ requires-dist = [
|
|||||||
{ name = "lxml", specifier = ">=5.3.0" },
|
{ name = "lxml", specifier = ">=5.3.0" },
|
||||||
{ name = "peewee", specifier = ">=3.17.8" },
|
{ name = "peewee", specifier = ">=3.17.8" },
|
||||||
{ name = "textual", specifier = ">=1.0.0" },
|
{ name = "textual", specifier = ">=1.0.0" },
|
||||||
{ name = "tomlkit", specifier = ">=0.13.2" },
|
|
||||||
{ name = "typer", specifier = ">=0.15.1" },
|
{ name = "typer", specifier = ">=0.15.1" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user