Compare commits
	
		
			No commits in common. "92cc3c5b40900c97851dd0b7d804687bbe9eb5d2" and "2c5310fe0d36f3eec9e9151ed8c1ed047d585357" have entirely different histories.
		
	
	
		
			92cc3c5b40
			...
			2c5310fe0d
		
	
		
					 13 changed files with 107 additions and 345 deletions
				
			
		|  | @ -9,7 +9,6 @@ dependencies = [ | |||
|   "lxml>=5.3.0", | ||||
|   "peewee>=3.17.8", | ||||
|   "textual>=1.0.0", | ||||
|   "tomlkit>=0.13.2", | ||||
|   "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 peewee import fn, JOIN | ||||
| from ..db import Task, Category | ||||
| from ..db import Task, Category, TaskStatus | ||||
| 
 | ||||
| 
 | ||||
| def get_category_summary(num: int = 5) -> list[dict]: | ||||
|  | @ -20,7 +20,7 @@ def get_category_summary(num: int = 5) -> list[dict]: | |||
|     overdue_count = ( | ||||
|         Task.select(Task.category, fn.COUNT(Task.id).alias("overdue")) | ||||
|         .where( | ||||
|             (~Task.deleted) & (Task.due < now) & (Task.status != "done") | ||||
|             (~Task.deleted) & (Task.due < now) & (Task.status != TaskStatus.DONE.value) | ||||
|         ) | ||||
|         .group_by(Task.category) | ||||
|     ) | ||||
|  | @ -32,7 +32,7 @@ def get_category_summary(num: int = 5) -> list[dict]: | |||
|             (~Task.deleted) | ||||
|             & (Task.due >= now) | ||||
|             & (Task.due <= week_from_now) | ||||
|             & (Task.status != "done") | ||||
|             & (Task.status != TaskStatus.DONE.value) | ||||
|         ) | ||||
|         .group_by(Task.category) | ||||
|     ) | ||||
|  | @ -41,16 +41,16 @@ def get_category_summary(num: int = 5) -> list[dict]: | |||
|     query = ( | ||||
|         Category.select( | ||||
|             Category.name, | ||||
|             fn.COALESCE(fn.SUM(Task.status == "zero"), 0).alias( | ||||
|             fn.COALESCE(fn.SUM(Task.status == TaskStatus.ZERO.value), 0).alias( | ||||
|                 "zero_count" | ||||
|             ), | ||||
|             fn.COALESCE(fn.SUM(Task.status == "wip"), 0).alias( | ||||
|             fn.COALESCE(fn.SUM(Task.status == TaskStatus.WIP.value), 0).alias( | ||||
|                 "wip_count" | ||||
|             ), | ||||
|             fn.COALESCE(fn.SUM(Task.status == "blocked"), 0).alias( | ||||
|             fn.COALESCE(fn.SUM(Task.status == TaskStatus.BLOCKED.value), 0).alias( | ||||
|                 "blocked_count" | ||||
|             ), | ||||
|             fn.COALESCE(fn.SUM(Task.status == "done"), 0).alias( | ||||
|             fn.COALESCE(fn.SUM(Task.status == TaskStatus.DONE.value), 0).alias( | ||||
|                 "done_count" | ||||
|             ), | ||||
|             fn.COALESCE(overdue_count.c.overdue, 0).alias("overdue"), | ||||
|  | @ -148,7 +148,7 @@ def get_due_soon( | |||
|             (~Task.deleted) | ||||
|             & (Task.due.is_null(False)) | ||||
|             & (Task.due != "") | ||||
|             & (Task.status != "done") | ||||
|             & (Task.status != TaskStatus.DONE.value) | ||||
|         ) | ||||
|     ) | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,8 +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 .. import config | ||||
| from ..db import db, Task, Category, TaskStatus, SavedSearch | ||||
| 
 | ||||
| 
 | ||||
| def category_lookup(category): | ||||
|  | @ -19,7 +18,7 @@ def get_task(item_id: int) -> Task: | |||
| def add_task( | ||||
|     text: str, | ||||
|     category: str, | ||||
|     status: str, | ||||
|     status: str = TaskStatus.ZERO.value, | ||||
|     due: datetime | None = None, | ||||
|     type: str = "", | ||||
| ) -> Task: | ||||
|  | @ -63,7 +62,7 @@ def _parse_sort_string(sort_string, status_order): | |||
| 
 | ||||
|         if field == "status": | ||||
|             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 | ||||
|             order_case = Case( | ||||
|                 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): | ||||
|     DAYS_BETWEEN = "days-btwn" | ||||
|     MONTHLY = "monthly" | ||||
|  | @ -39,7 +48,10 @@ class Category(BaseModel): | |||
| 
 | ||||
| class Task(BaseModel): | ||||
|     text = TextField() | ||||
|     status = CharField() | ||||
|     status = CharField( | ||||
|         choices=[(status.value, status.name) for status in TaskStatus], | ||||
|         default=TaskStatus.ZERO.value, | ||||
|     ) | ||||
|     due = DateTimeField(null=True) | ||||
|     category = ForeignKeyField(Category, backref="tasks", null=True) | ||||
|     type = CharField() | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| import csv | ||||
| from datetime import datetime | ||||
| from tt.db import initialize_db, Task, Category | ||||
| from tt.config import STATUSES | ||||
| from tt.db import initialize_db, Task, Category, TaskStatus | ||||
| 
 | ||||
| 
 | ||||
| def import_tasks_from_csv(filename: str): | ||||
|  | @ -21,7 +20,9 @@ def import_tasks_from_csv(filename: str): | |||
| 
 | ||||
|             # Validate status | ||||
|             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'") | ||||
|                 status = "zero" | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,3 @@ | |||
| import datetime | ||||
| from textual.app import App | ||||
| from textual.widgets import ( | ||||
|     DataTable, | ||||
|  | @ -11,15 +10,30 @@ from textual.containers import Container | |||
| from ..utils import ( | ||||
|     remove_rich_tag, | ||||
|     filter_to_string, | ||||
|     advance_enum_val, | ||||
|     get_text_from_editor, | ||||
| ) | ||||
| from .keymodal import KeyModal | ||||
| from .modals import ChoiceModal, DateModal, ConfirmModal | ||||
| 
 | ||||
| ELLIPSIS = "…" | ||||
| 
 | ||||
| 
 | ||||
| class NotifyValidationError(Exception): | ||||
|     """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: | ||||
|     def __init__( | ||||
|  | @ -28,6 +42,8 @@ class TableColumnConfig: | |||
|         display_name: str, | ||||
|         *, | ||||
|         default=None, | ||||
|         enum=None, | ||||
|         preprocessor=None, | ||||
|         enable_editor=False, | ||||
|         filterable=True, | ||||
|         read_only=False, | ||||
|  | @ -35,57 +51,16 @@ class TableColumnConfig: | |||
|         self.field = field | ||||
|         self.display_name = display_name | ||||
|         self.default = default | ||||
|         self.enum = enum | ||||
|         self.enable_editor = enable_editor | ||||
|         self.filterable = filterable | ||||
|         self.read_only = read_only | ||||
| 
 | ||||
|     def preprocess(self, val): | ||||
|         return val  # no-op | ||||
| 
 | ||||
|     def start_change(self, app, current_value):     | ||||
|         if current_value.endswith(ELLIPSIS): | ||||
|             app.action_start_edit() | ||||
|         if preprocessor: | ||||
|             self.preprocessor = preprocessor | ||||
|         elif self.enum: | ||||
|             self.preprocessor = _enum_preprocessor(self.enum) | ||||
|         else: | ||||
|             # default edit mode | ||||
|             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 = "…" | ||||
| 
 | ||||
|             self.preprocessor = lambda x: x | ||||
| 
 | ||||
| 
 | ||||
| class TableEditor(App): | ||||
|  | @ -159,7 +134,7 @@ class TableEditor(App): | |||
|         ("?", "show_keys", "show keybindings"), | ||||
|     ] | ||||
| 
 | ||||
|     def __init__(self): | ||||
|     def __init__(self, default_view="default"): | ||||
|         super().__init__() | ||||
|         self.filters = {} | ||||
|         self.sort_string = ""  # TODO: default sort | ||||
|  | @ -237,10 +212,7 @@ class TableEditor(App): | |||
|             self.table.move_cursor(row=0, column=1) | ||||
| 
 | ||||
|     def action_delete_item(self): | ||||
|         self.push_screen(ConfirmModal(f"delete ?"), self._delete_item_callback) | ||||
| 
 | ||||
|     def _delete_item_callback(self, confirm): | ||||
|         if confirm and self.table.cursor_column == 0: | ||||
|         if self.table.cursor_column == 0: | ||||
|             cur_row = self.table.cursor_row | ||||
|             item_id = int(self.table.get_cell_at((cur_row, 0))) | ||||
|             # deletable items need a delete | ||||
|  | @ -257,8 +229,10 @@ class TableEditor(App): | |||
|                 continue | ||||
|             val = self.filters.get(fc.field, fc.default) | ||||
|             if val is not None: | ||||
|                 if "," in val: | ||||
|                     val = val.split(",")[0] # TODO: fix hack for enums | ||||
|                 # enums use comma separated filters | ||||
|                 if fc.enum: | ||||
|                     prepopulated[fc.field] = val.split(",")[0] | ||||
|                 else: | ||||
|                     prepopulated[fc.field] = val | ||||
| 
 | ||||
|         new_item = self.add_item_callback(**prepopulated) | ||||
|  | @ -273,21 +247,21 @@ class TableEditor(App): | |||
|     def _active_item_id(self): | ||||
|         return int(self.table.get_cell_at((self.table.cursor_row, 0))) | ||||
| 
 | ||||
|     # def action_toggle_cell(self): | ||||
|     #     cur_row = self.table.cursor_row | ||||
|     #     cur_col = self.table.cursor_column | ||||
|     #     cconf = self._active_column_config() | ||||
|     def action_toggle_cell(self): | ||||
|         cur_row = self.table.cursor_row | ||||
|         cur_col = self.table.cursor_column | ||||
|         cconf = self._active_column_config() | ||||
| 
 | ||||
|     #     if cconf.enum: | ||||
|     #         item_id = self._active_item_id() | ||||
|     #         current_val = self.table.get_cell_at((cur_row, cur_col)) | ||||
|     #         next_val = advance_enum_val(cconf.enum, current_val) | ||||
|     #         self.table.update_cell_at((cur_row, cur_col), next_val) | ||||
|     #         # trigger item_id to be saved on the next cursor move | ||||
|     #         # this avoids filtered columns disappearing right away | ||||
|     #         # and tons of DB writes | ||||
|     #         update = {cconf.field: next_val} | ||||
|     #         self._register_save_on_move(item_id, **update) | ||||
|         if cconf.enum: | ||||
|             item_id = self._active_item_id() | ||||
|             current_val = self.table.get_cell_at((cur_row, cur_col)) | ||||
|             next_val = advance_enum_val(cconf.enum, current_val) | ||||
|             self.table.update_cell_at((cur_row, cur_col), next_val) | ||||
|             # trigger item_id to be saved on the next cursor move | ||||
|             # this avoids filtered columns disappearing right away | ||||
|             # and tons of DB writes | ||||
|             update = {cconf.field: next_val} | ||||
|             self._register_save_on_move(item_id, **update) | ||||
| 
 | ||||
|     def _register_save_on_move(self, item_id, **kwargs): | ||||
|         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: | ||||
|             return | ||||
| 
 | ||||
|         # save cursor before callback, so correct position updates | ||||
|         self._save_cursor() | ||||
|         current_value = self.table.get_cell_at( | ||||
|             (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) | ||||
| 
 | ||||
|         # delegate to start_change, which will call back to self | ||||
|         cconf.start_change(self, current_value) | ||||
|         self._show_input("edit", current_value) | ||||
| 
 | ||||
|     def action_start_edit(self): | ||||
|         cconf = self._active_column_config() | ||||
|  | @ -418,7 +392,7 @@ class TableEditor(App): | |||
| 
 | ||||
|         # preprocess/validate the field being saved | ||||
|         try: | ||||
|             update_data[field] = cconf.preprocess(new_value) | ||||
|             update_data[field] = cconf.preprocessor(new_value) | ||||
|             self.update_item_callback(item_id, **update_data) | ||||
|             self.refresh_data() | ||||
|         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 ( | ||||
|     TableEditor, | ||||
|     TableColumnConfig, | ||||
|     EnumColumnConfig, | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -19,14 +18,14 @@ class TaskGenEditor(TableEditor): | |||
|     TABLE_CONFIG = ( | ||||
|         TableColumnConfig("id", "ID"), | ||||
|         TableColumnConfig("template", "Template", default="recur {val}"), | ||||
|         EnumColumnConfig( | ||||
|         TableColumnConfig( | ||||
|             "type", | ||||
|             "Type", | ||||
|             default=GeneratorType.DAYS_BETWEEN.value, | ||||
|             enum=GeneratorType, | ||||
|         ), | ||||
|         TableColumnConfig("val", "Value", default="1"), | ||||
|         TableColumnConfig("next_at", "Next @", read_only=True), | ||||
|         TableColumnConfig("next_at", "Next @", default="", read_only=True), | ||||
|     ) | ||||
| 
 | ||||
|     def __init__(self): | ||||
|  |  | |||
|  | @ -2,40 +2,47 @@ import json | |||
| from textual.widgets import Input | ||||
| from datetime import datetime | ||||
| 
 | ||||
| from .. import config | ||||
| from ..controller.tasks import ( | ||||
|     get_task, | ||||
|     get_tasks, | ||||
|     add_task, | ||||
|     update_task, | ||||
|     TaskStatus, | ||||
|     save_view, | ||||
|     get_saved_view, | ||||
| ) | ||||
| from ..utils import ( | ||||
|     get_color_enum, | ||||
|     get_colored_category, | ||||
|     get_colored_status, | ||||
|     get_colored_date, | ||||
| ) | ||||
| from .editor import ( | ||||
|     TableEditor, | ||||
|     TableColumnConfig, | ||||
|     EnumColumnConfig, | ||||
|     DateColumnConfig, | ||||
|     NotifyValidationError, | ||||
|     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): | ||||
|     TABLE_CONFIG = ( | ||||
|         TableColumnConfig("id", "ID"), | ||||
|         TableColumnConfig("text", "Task", default="new task", enable_editor=True), | ||||
|         EnumColumnConfig( | ||||
|         TableColumnConfig( | ||||
|             "status", | ||||
|             "Status", | ||||
|             enum=config.STATUSES, | ||||
|             default="zero", | ||||
|             enum=TaskStatus, | ||||
|         ), | ||||
|         TableColumnConfig("type", "Type", default=""), | ||||
|         DateColumnConfig("due", "Due", default=""), | ||||
|         TableColumnConfig("due", "Due", default="", preprocessor=due_preprocessor), | ||||
|         TableColumnConfig("category", "Category", default="main"), | ||||
|     ) | ||||
| 
 | ||||
|  | @ -89,12 +96,10 @@ class TT(TableEditor): | |||
|             sort=self.sort_string, | ||||
|         ) | ||||
|         for item in items: | ||||
|             category = get_color_enum( | ||||
|                 item.category.name if item.category else " - ", | ||||
|                 config.PROJECTS, | ||||
|                 "grey" | ||||
|             category = get_colored_category( | ||||
|                 item.category.name if item.category else " - " | ||||
|             ) | ||||
|             status = get_color_enum(item.status, config.STATUSES, "red") | ||||
|             status = get_colored_status(item.status) | ||||
|             due = get_colored_date(item.due) | ||||
| 
 | ||||
|             if "\n" in item.text: | ||||
|  |  | |||
|  | @ -32,9 +32,15 @@ def advance_enum_val(enum_type, cur_val): | |||
|     return members[next_idx] | ||||
| 
 | ||||
| 
 | ||||
| def get_color_enum(value: str, enum: dict[str, dict], default: str) -> str: | ||||
|     color = enum.get(value, {"color": default})["color"] | ||||
|     return f"[{color}]{value}[/]" | ||||
| def get_colored_status(status: str) -> str: | ||||
|     colors = { | ||||
|         "zero": "#666666", | ||||
|         "wip": "#33aa99", | ||||
|         "blocked": "#cc9900", | ||||
|         "done": "#009900", | ||||
|     } | ||||
|     color = colors.get(status, "#666666") | ||||
|     return f"[{color}]{status}[/]" | ||||
| 
 | ||||
| 
 | ||||
| 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
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								uv.lock
									
									
									
									
									
								
							|  | @ -1,5 +1,4 @@ | |||
| version = 1 | ||||
| revision = 1 | ||||
| requires-python = ">=3.10" | ||||
| 
 | ||||
| [[package]] | ||||
|  | @ -161,7 +160,7 @@ name = "click" | |||
| version = "8.1.8" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| 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 } | ||||
| 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 }, | ||||
| ] | ||||
| 
 | ||||
| [[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]] | ||||
| name = "tt" | ||||
| version = "0.1.0" | ||||
|  | @ -920,7 +910,6 @@ dependencies = [ | |||
|     { name = "lxml" }, | ||||
|     { name = "peewee" }, | ||||
|     { name = "textual" }, | ||||
|     { name = "tomlkit" }, | ||||
|     { name = "typer" }, | ||||
| ] | ||||
| 
 | ||||
|  | @ -937,7 +926,6 @@ requires-dist = [ | |||
|     { name = "lxml", specifier = ">=5.3.0" }, | ||||
|     { name = "peewee", specifier = ">=3.17.8" }, | ||||
|     { name = "textual", specifier = ">=1.0.0" }, | ||||
|     { name = "tomlkit", specifier = ">=0.13.2" }, | ||||
|     { name = "typer", specifier = ">=0.15.1" }, | ||||
| ] | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue