Compare commits

..

No commits in common. "fea33eb62eb5b853dac5064beb74c2a290a9e004" and "74ed6516b4a59e1ba05b8121ceac5657bb93dd34" have entirely different histories.

10 changed files with 131 additions and 207 deletions

2
.gitignore vendored
View File

@ -1,2 +1,2 @@
*.pyc
*.db*
*.db

View File

@ -1,10 +0,0 @@
SPECIAL_DATES_PIECES = {
"future": (3000,1,1),
"unclassified": (1999,1,1),
}
SPECIAL_DATES_DISPLAY = {
"3000-01-01": "[#333333]future[/]",
"1999-01-01": "[#cccccc]unclassified[/]",
}

View File

@ -4,7 +4,7 @@ from peewee import fn
from peewee import Case, Value
from ..db import db, Task, SavedSearch
from .. import config
from ..constants import SPECIAL_DATES_PIECES
def get_task(item_id: int) -> Task:
@ -14,7 +14,7 @@ def get_task(item_id: int) -> Task:
def add_task(
text: str,
status: str,
due: datetime | str = SPECIAL_DATES_PIECES["unclassified"],
due: datetime | None = None,
type: str = "",
project: str = "",
) -> Task:
@ -84,8 +84,8 @@ def get_tasks(
query = query.where(fn.Lower(Task.text).contains(search_text.lower()))
if statuses:
query = query.where(Task.status.in_(statuses))
if projects:
query = query.where(Task.project.in_(projects))
#if projects:
# query = query.where(Task.project.in_(projects))
sort_expressions = _parse_sort_string(sort, statuses)
query = query.order_by(*sort_expressions)

View File

@ -1,89 +0,0 @@
import datetime
from ..utils import (
get_color_enum,
get_colored_date,
)
from .modals import ChoiceModal, DateModal
class NotifyValidationError(Exception):
"""will notify and continue if raised"""
ELLIPSIS = ""
class TableColumnConfig:
def __init__(
self,
field: str,
display_name: str,
*,
default=None,
enable_editor=False,
filterable=True,
read_only=False,
):
self.field = field
self.display_name = display_name
self.default = default
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()
else:
# default edit mode
app._show_input("edit", current_value)
def format_for_display(self, val):
val = str(val)
if "\n" in val:
val = val.split("\n")[0] + ELLIPSIS
return val
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 format_for_display(self, val):
return get_color_enum(val, 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.datetime.strptime(val, "%Y-%m-%d")
except ValueError:
raise NotifyValidationError("Invalid date format. Use YYYY-MM-DD")
def format_for_display(self, val):
return get_colored_date(val)
def start_change(self, app, current_value):
app.push_screen(DateModal(current_value), app.apply_change)
def get_col_cls(field_type):
return {
"text": TableColumnConfig,
"enum": EnumColumnConfig,
"date": DateColumnConfig,
}[field_type]

View File

@ -1,3 +1,4 @@
import datetime
from textual.app import App
from textual.widgets import (
DataTable,
@ -12,10 +13,86 @@ from ..utils import (
remove_rich_tag,
filter_to_string,
get_text_from_editor,
get_color_enum,
get_colored_date,
)
from .keymodal import KeyModal
from .modals import ConfirmModal
from .columns import get_col_cls
from .modals import ChoiceModal, DateModal, ConfirmModal
class NotifyValidationError(Exception):
"""will notify and continue if raised"""
ELLIPSIS = ""
class TableColumnConfig:
def __init__(
self,
field: str,
display_name: str,
*,
default=None,
enable_editor=False,
filterable=True,
read_only=False,
):
self.field = field
self.display_name = display_name
self.default = default
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()
else:
# default edit mode
app._show_input("edit", current_value)
def format_for_display(self, val):
val = str(val)
if "\n" in val:
val = val.split("\n")[0] + ELLIPSIS
return val
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 format_for_display(self, val):
return get_color_enum(val, 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 format_for_display(self, val):
return get_colored_date(val)
def start_change(self, app, current_value):
app.push_screen(DateModal(current_value), app.apply_change)
@ -110,7 +187,11 @@ class TableEditor(App):
# set up columns
for col in view["columns"]:
field_type = col.get("field_type", "text")
field_cls = get_col_cls(field_type)
field_cls = {
"text": TableColumnConfig,
"enum": EnumColumnConfig,
"date": DateColumnConfig,
}[field_type]
field_name = col["field_name"]
display_name = col.get("display_name", field_name.title())
default = col.get("default")

View File

@ -37,7 +37,6 @@ class KeyModal(ModalScreen):
for binding in self.app.BINDINGS:
if binding[0] not in ["h", "j", "k", "l", "g", "G", "escape"]:
table.add_row(binding[0], binding[2])
# TODO: MRO?
yield Static("tt keybindings", classes="title")
yield Static(table)

View File

@ -1,12 +1,8 @@
import datetime
from textual.screen import ModalScreen
from textual.binding import Binding
from textual.widgets import Label
from textual.containers import Horizontal, Vertical
from textual.reactive import reactive
from textual.widgets import RadioSet, RadioButton, Label
from .. import config
from ..utils import get_color_enum
from ..constants import SPECIAL_DATES_PIECES
class ConfirmModal(ModalScreen):
CSS = """
@ -46,56 +42,42 @@ class ChoiceModal(ModalScreen):
ChoiceModal Label {
height: 1;
}
ChoiceModal Label#selected {
background: white;
}
"""
BINDINGS = [
("j,tab", "cursor_down", "Down"),
("k,shift+tab", "cursor_up", "Up"),
Binding("enter", "select", "Select", priority=True),
("k", "cursor_up", "Up"),
Binding("enter", "select", "Select", priority=False),
("escape", "cancel", "cancel"),
]
def __init__(self, enum, selected):
self._enum = enum
self.enum_by_idx = list(self._enum)
# selection index
self.sel_idx = 0
# convert value back to index for initial selection
for idx, e in enumerate(self._enum):
if e.value == selected:
self.sel_idx = idx
break
self.selected = selected
super().__init__()
def compose(self):
for idx, e in enumerate(self._enum):
yield Label(
("> " if idx == self.sel_idx else " ") +
get_color_enum(e.value, self._enum),
classes="selected" if idx == self.sel_idx else "",
)
def _move_cursor(self, dir):
labels = self.query(Label)
# reset old
labels[self.sel_idx].update(" " + get_color_enum(self.enum_by_idx[self.sel_idx], self._enum))
# move cursor
self.sel_idx = (self.sel_idx + dir) % len(self._enum)
# reset new
labels[self.sel_idx].update("> " + get_color_enum(self.enum_by_idx[self.sel_idx], self._enum))
yield RadioSet(
*[
RadioButton(
get_color_enum(e.value, config.STATUSES), value=self.selected == str(e.value)
)
for e in self._enum
]
)
def action_cursor_down(self):
self._move_cursor(1)
self.query_one(RadioSet).action_next_button()
def action_cursor_up(self):
self._move_cursor(-1)
self.query_one(RadioSet).action_previous_button()
async def action_select(self):
self.dismiss(self.enum_by_idx[self.sel_idx])
def action_select(self):
rs = self.query_one(RadioSet)
# TODO: this doesn't work
#rs.action_toggle_button()
pressed = rs.pressed_button
self.dismiss(str(pressed.label))
def action_cancel(self):
self.app.pop_screen()
@ -104,71 +86,37 @@ class ChoiceModal(ModalScreen):
class DateModal(ModalScreen):
CSS = """
DateModal {
layout: horizontal;
align: center middle;
background: $primary 30%;
}
DateModal Vertical {
border: double teal;
height: 10;
width: 50;
}
DateModal Horizonal {
}
DateModal Label {
border: solid white;
align: center middle;
border: solid grey;
}
DateModal Label.selected-date {
border: solid green;
}
DateModal Label.hints {
border: solid grey;
height: 4;
}
"""
BINDINGS = [
("j", "cursor_down", "Down"),
("k", "cursor_up", "Up"),
("h,shift+tab", "cursor_left", "Left"),
("l,tab", "cursor_right", "Right"),
("f", "future", "Future"),
("t", "today", "Today"),
("u", "unclassified", "Unclassified"),
("h,tab", "cursor_left", "Left"),
("l", "cursor_right", "Right"),
Binding("enter", "select", "Select", priority=True),
("escape", "cancel", "cancel"),
]
pieces = reactive([0, 0, 0], recompose=True)
def __init__(self, date):
super().__init__()
if date in SPECIAL_DATES_PIECES:
self.pieces = list(SPECIAL_DATES_PIECES[date])
elif date:
self.pieces = [int(p) for p in date.split("-")]
else:
self.action_today()
self.pieces = [int(p) for p in date.split("-")]
self.selected = 1 # start on month
super().__init__()
def compose(self):
with Vertical():
with Horizontal():
yield Label(f"{self.pieces[0]}")
yield Label(f"{self.pieces[1]}", classes="selected-date")
yield Label(f"{self.pieces[2]}")
yield Label("""(h/j/k/l) move (enter) confirm (esc) quit
(p)ast (t)oday (f)uture""", classes="hints")
def action_future(self):
self.pieces = list(SPECIAL_DATES_PIECES["future"])
def action_unclassified(self):
self.pieces = list(SPECIAL_DATES_PIECES["unclassified"])
def action_today(self):
today = datetime.date.today()
self.pieces = [today.year, today.month, today.day]
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
@ -204,7 +152,8 @@ class DateModal(ModalScreen):
if cur_value > self.max_for(self.selected):
cur_value = 1
self.pieces[self.selected] = cur_value
self.mutate_reactive(DateModal.pieces)
cur_label = self.query("Label")[self.selected]
cur_label.update(str(cur_value))
def action_cursor_down(self):
self._move_piece(-1)
@ -229,7 +178,8 @@ class DateModal(ModalScreen):
event.prevent_default()
def action_select(self):
self.dismiss("-".join(str(p) for p in self.pieces))
date = "-".join(str(p) for p in self.pieces)
self.dismiss(date)
def action_cancel(self):
self.app.pop_screen()

View File

@ -56,8 +56,10 @@ class TT(TableEditor):
def refresh_items(self):
items = get_tasks(
self.search_query,
projects=self._filters_to_list("project"),
statuses=self._filters_to_list("status"),
projects=self.filters.get("project", "").split(","),
statuses=self.filters.get("status", "").split(",")
if "status" in self.filters
else None,
sort=self.sort_string,
)
for item in items:
@ -66,12 +68,6 @@ class TT(TableEditor):
key=str(item.id),
)
def _filters_to_list(self, key):
filters = self.filters.get(key)
if filters:
return filters.split(",")
else:
return None
def run(default_view):
app = TT(default_view)

View File

@ -3,7 +3,6 @@ import os
import datetime
import tempfile
import subprocess
from .constants import SPECIAL_DATES_DISPLAY
def filter_to_string(filters, search_query):
@ -42,8 +41,6 @@ def get_colored_date(date: datetime.date) -> str:
if not isinstance(date, datetime.date):
return ""
as_str = date.strftime("%Y-%m-%d")
if as_str in SPECIAL_DATES_DISPLAY:
return SPECIAL_DATES_DISPLAY[as_str]
today = datetime.date.today()
if date.date() < today:
return f"[#eeeeee on #dd1111]{as_str}[/]"

View File

@ -28,7 +28,7 @@ values = [
[[views]]
name = "tasks"
sort = "due,status"
sort = "due"
[views.filters]
status = "wip,blocked,zero"
@ -40,7 +40,7 @@ read_only = true
[[views.columns]]
field_name = "text"
display_name = "Task"
default = "new task"
default = "new taskz"
editor = true
[[views.columns]]
@ -56,7 +56,7 @@ default = ""
[[views.columns]]
field_name = "due"
field_type = "date"
default = "1999-01-01"
default = ""
[[views.columns]]
field_name = "project"