diff --git a/src/tt/tui/editor.py b/src/tt/tui/editor.py index 90cd8e8..fe315c8 100644 --- a/src/tt/tui/editor.py +++ b/src/tt/tui/editor.py @@ -14,7 +14,7 @@ from ..utils import ( get_text_from_editor, ) from .keymodal import KeyModal -from .modals import ChoiceModal #DateModal +from .modals import ChoiceModal ELLIPSIS = "…" @@ -23,20 +23,6 @@ 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 {val}. Use: {[s.value for s in enumCls]}" - ) - - return preprocessor - class TableColumnConfig: def __init__( @@ -45,8 +31,6 @@ class TableColumnConfig: display_name: str, *, default=None, - enum=None, - preprocessor=None, enable_editor=False, filterable=True, read_only=False, @@ -54,16 +38,45 @@ 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 - if preprocessor: - self.preprocessor = preprocessor - elif self.enum: - self.preprocessor = _enum_preprocessor(self.enum) + + def preprocess(self, val): + return val # no-op + + def start_change(self, app, current_value): + if current_value.endswith(ELLIPSIS): + app.action_start_edit() else: - self.preprocessor = lambda x: x + # 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.enumCls = enum + + def preprocess(self, val): + try: + self.enumCls(val) + return val + except ValueError: + raise NotifyValidationError( + f"Invalid value {val}. Use: {[s.value for s in self.enumCls]}" + ) + + def start_change(self, app, current_value): + # a weird hack? pass app here and correct modal gets pushed + app.push_screen(ChoiceModal(self.enumCls, current_value), app.apply_change) + class TableEditor(App): @@ -341,16 +354,10 @@ class TableEditor(App): current_value = self.table.get_cell_at( (self.table.cursor_row, self.table.cursor_column) ) + current_value = remove_rich_tag(current_value) - if cconf.enum: - self.push_screen(ChoiceModal(cconf.enum, current_value), - self.apply_change) - elif current_value.endswith(ELLIPSIS): - self.action_start_edit() - else: - # default edit mode - 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): cconf = self._active_column_config() @@ -401,7 +408,7 @@ class TableEditor(App): # preprocess/validate the field being saved try: - update_data[field] = cconf.preprocessor(new_value) + update_data[field] = cconf.preprocess(new_value) self.update_item_callback(item_id, **update_data) self.refresh_data() except NotifyValidationError as e: diff --git a/src/tt/tui/modals.py b/src/tt/tui/modals.py index d212165..05993ce 100644 --- a/src/tt/tui/modals.py +++ b/src/tt/tui/modals.py @@ -1,11 +1,9 @@ from textual.screen import ModalScreen -from textual.containers import Grid from textual.binding import Binding - from textual.widgets import RadioSet, RadioButton, Label -class ChoiceModal(ModalScreen): +class ChoiceModal(ModalScreen): CSS = """ ChoiceModal { align: center middle; @@ -19,10 +17,9 @@ class ChoiceModal(ModalScreen): BINDINGS = [ ("j", "cursor_down", "Down"), ("k", "cursor_up", "Up"), - # TODO: get this to work as override Binding("enter", "select", "Select", priority=True), ("c", "select", "Select"), - ("escape", "cancel", "cancel") + ("escape", "cancel", "cancel"), ] def __init__(self, enum, selected): @@ -34,7 +31,8 @@ class ChoiceModal(ModalScreen): yield Label(f"{self._enum.__name__}") yield RadioSet( *[ - RadioButton(str(e.value), value=self.selected==str(e.value)) for e in self._enum + RadioButton(str(e.value), value=self.selected == str(e.value)) + for e in self._enum ] ) @@ -43,7 +41,7 @@ class ChoiceModal(ModalScreen): 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() @@ -52,3 +50,105 @@ class ChoiceModal(ModalScreen): 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() diff --git a/src/tt/tui/recurring.py b/src/tt/tui/recurring.py index 249df88..539764b 100644 --- a/src/tt/tui/recurring.py +++ b/src/tt/tui/recurring.py @@ -11,6 +11,7 @@ from ..db import GeneratorType from .editor import ( TableEditor, TableColumnConfig, + EnumColumnConfig, ) @@ -18,7 +19,7 @@ class TaskGenEditor(TableEditor): TABLE_CONFIG = ( TableColumnConfig("id", "ID"), TableColumnConfig("template", "Template", default="recur {val}"), - TableColumnConfig( + EnumColumnConfig( "type", "Type", default=GeneratorType.DAYS_BETWEEN.value, diff --git a/src/tt/tui/tasks.py b/src/tt/tui/tasks.py index 97acbca..539add6 100644 --- a/src/tt/tui/tasks.py +++ b/src/tt/tui/tasks.py @@ -15,34 +15,40 @@ from ..utils import ( get_colored_category, get_colored_status, get_colored_date, + remove_rich_tag, ) from .editor import ( TableEditor, TableColumnConfig, + EnumColumnConfig, NotifyValidationError, ELLIPSIS, ) +from .modals import DateModal +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 due_preprocessor(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) class TT(TableEditor): TABLE_CONFIG = ( TableColumnConfig("id", "ID"), TableColumnConfig("text", "Task", default="new task", enable_editor=True), - TableColumnConfig( + EnumColumnConfig( "status", "Status", - default="zero", enum=TaskStatus, + default="zero", ), TableColumnConfig("type", "Type", default=""), - TableColumnConfig("due", "Due", default="", preprocessor=due_preprocessor), + DateColumnConfig("due", "Due", default=""), TableColumnConfig("category", "Category", default="main"), )