taskai-cli 1.9.26__tar.gz → 1.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/CHANGELOG.md +24 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/DEVPLAN.md +1 -1
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/PKG-INFO +6 -4
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/README.md +5 -3
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/docs/_static/demo-script.md +2 -2
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/docs/browser-mode.md +1 -1
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/docs/configuration.md +4 -2
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/docs/getting-started.md +1 -1
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/docs/index.md +1 -1
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/docs/item-data.md +27 -4
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/docs/managing-the-tree.md +3 -3
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/pyproject.toml +1 -1
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/cli.py +33 -10
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/dates.py +2 -2
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/filters.py +2 -2
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/help_menu.py +13 -5
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/json_dir_database.py +25 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/models.py +2 -2
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/static/editpanel.js +4 -4
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/static/graph.js +1 -1
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/static/navigation.js +9 -7
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/static/render.js +2 -2
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/views.py +51 -8
- taskai_cli-1.10.0/test/test_legacy_migration.py +73 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/test/test_view.py +1 -1
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/.github/workflows/test.yml +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/.gitignore +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/.readthedocs.yaml +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/DEVLOG.md +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/PHASE_1_RELEASE.md +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/TRIAGE.md +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/docs/.gitignore +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/docs/_static/.gitkeep +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/docs/ai-integration.md +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/docs/commands.md +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/docs/conf.py +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/docs/interactive-mode.md +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/docs/requirements.txt +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/scripts/bump_version.py +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/browser.py +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/config.py +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/errors.py +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/llm_models.py +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/services/ai.py +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/services/pomodoro.py +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/services/repair_database.py +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/services/user_setup.py +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/static/camera.js +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/static/canvas.js +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/static/config.js +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/static/console.js +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/static/index.html +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/static/shortcuts.js +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/taskai/static/style.css +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/test/test_chains.py +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/test/test_cli.py +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/test/test_execution.py +0 -0
- {taskai_cli-1.9.26 → taskai_cli-1.10.0}/test/test_json_dir_database.py +0 -0
|
@@ -6,6 +6,30 @@ history. Versions before 1.6.6 predate this file and aren't reconstructed
|
|
|
6
6
|
here in detail — see `DEVLOG.md` for the full engineering history back to
|
|
7
7
|
the project's start.
|
|
8
8
|
|
|
9
|
+
## [1.10.0] - Unreleased
|
|
10
|
+
|
|
11
|
+
CLI expansion.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- A dedicated command for every item field: `task due 10 tomorrow`,
|
|
15
|
+
`task description 10 ...`, `task priority`, `task color` (`task status`
|
|
16
|
+
already existed). The value needn't be quoted, and leaving it off clears
|
|
17
|
+
the field. `task name` is now an alias for `task rename`.
|
|
18
|
+
- `task description {id|name} -a {text}` appends to the existing description
|
|
19
|
+
(on a new line) instead of replacing it
|
|
20
|
+
|
|
21
|
+
- `task show` draws an item in its `color` (the same per-item color the
|
|
22
|
+
browser view uses), overriding the `DISPLAY_COLORS` columns for that item
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- The CLI shows dates as just the day, `MM/DD/YY` (due dates, `created_on`,
|
|
26
|
+
comment timestamps) - the time of day is no longer displayed
|
|
27
|
+
- `due_by` is now just `due` everywhere: `--due`, `task show 'due<...'`, the
|
|
28
|
+
`DISPLAY_STRING` attribute, and the item JSON. Existing databases upgrade
|
|
29
|
+
themselves on load - a stored `due_by` is read in as `due` (only when `due`
|
|
30
|
+
isn't already set) and written back out under the new name, and a saved
|
|
31
|
+
`DISPLAY_STRING` that names `due_by` is rewritten too.
|
|
32
|
+
|
|
9
33
|
## [Unreleased]
|
|
10
34
|
|
|
11
35
|
The v1 release polish pass — see `PHASE_1_RELEASE.md` for the full working
|
|
@@ -19,7 +19,7 @@ generic tree node: `parent_id` + `child_ids` form the hierarchy (a root item
|
|
|
19
19
|
has `parent_id = None`); any item can be a parent of any other item to any
|
|
20
20
|
depth, and the same node type is used whether it's acting as a "list" or a
|
|
21
21
|
"task." Other `TodoItem` fields: `dependency_ids` (blocks-on), `linked_ids`
|
|
22
|
-
(soft link, not reparenting), `comment_ids`, `
|
|
22
|
+
(soft link, not reparenting), `comment_ids`, `due`, `priority`, `status`,
|
|
23
23
|
`completed`, `description`, and unused-so-far recurrence fields
|
|
24
24
|
(`recurs_every`/`recurs_until`/`recur_keep_incomplete`). `Comment` is a
|
|
25
25
|
simple `{content, item_id, created_on}`. `UserData` is the whole-DB
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: taskai-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.0
|
|
4
4
|
Author-email: Alex Paskal <alexcpaskal@gmail.com>
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: fastapi>=0.141.1
|
|
@@ -55,7 +55,8 @@ task link <parent> <item> soft-link an item under a second parent
|
|
|
55
55
|
task unlink <parent> <item> remove a soft-link
|
|
56
56
|
task reorder <id1> before|after <id2>
|
|
57
57
|
|
|
58
|
-
task
|
|
58
|
+
task <field> <id> <value> set one field: description, due, priority, status, color
|
|
59
|
+
(leave the value off to clear it)
|
|
59
60
|
task pomo <on_mins> <off_mins> pomodoro timer
|
|
60
61
|
|
|
61
62
|
task ai <prompt> natural language → commands
|
|
@@ -81,10 +82,11 @@ Pass any of these as `--field value` to `create`, `add`, or `update`:
|
|
|
81
82
|
| Field | Format | Example |
|
|
82
83
|
|---|---|---|
|
|
83
84
|
| `description` | string | `--description "needs review"` |
|
|
84
|
-
| `
|
|
85
|
+
| `due` | MM-DD-YYYY | `--due 08-15-2026` |
|
|
85
86
|
| `priority` | integer | `--priority 2` |
|
|
86
87
|
| `status` | string | `--status "in progress"` |
|
|
87
88
|
| `completed` | bool | `--completed true` |
|
|
89
|
+
| `color` | CSS color | `--color "#ff8844"` |
|
|
88
90
|
|
|
89
91
|
---
|
|
90
92
|
|
|
@@ -154,7 +156,7 @@ task create "Launch website"
|
|
|
154
156
|
# add subtasks (by name or id)
|
|
155
157
|
task add "Launch website" "Write copy"
|
|
156
158
|
task add "Launch website" "Design mockups" --priority 1
|
|
157
|
-
task add "Launch website" "Set up hosting" --
|
|
159
|
+
task add "Launch website" "Set up hosting" --due 08-15-2026
|
|
158
160
|
|
|
159
161
|
# check your work
|
|
160
162
|
task show "Launch website"
|
|
@@ -42,7 +42,8 @@ task link <parent> <item> soft-link an item under a second parent
|
|
|
42
42
|
task unlink <parent> <item> remove a soft-link
|
|
43
43
|
task reorder <id1> before|after <id2>
|
|
44
44
|
|
|
45
|
-
task
|
|
45
|
+
task <field> <id> <value> set one field: description, due, priority, status, color
|
|
46
|
+
(leave the value off to clear it)
|
|
46
47
|
task pomo <on_mins> <off_mins> pomodoro timer
|
|
47
48
|
|
|
48
49
|
task ai <prompt> natural language → commands
|
|
@@ -68,10 +69,11 @@ Pass any of these as `--field value` to `create`, `add`, or `update`:
|
|
|
68
69
|
| Field | Format | Example |
|
|
69
70
|
|---|---|---|
|
|
70
71
|
| `description` | string | `--description "needs review"` |
|
|
71
|
-
| `
|
|
72
|
+
| `due` | MM-DD-YYYY | `--due 08-15-2026` |
|
|
72
73
|
| `priority` | integer | `--priority 2` |
|
|
73
74
|
| `status` | string | `--status "in progress"` |
|
|
74
75
|
| `completed` | bool | `--completed true` |
|
|
76
|
+
| `color` | CSS color | `--color "#ff8844"` |
|
|
75
77
|
|
|
76
78
|
---
|
|
77
79
|
|
|
@@ -141,7 +143,7 @@ task create "Launch website"
|
|
|
141
143
|
# add subtasks (by name or id)
|
|
142
144
|
task add "Launch website" "Write copy"
|
|
143
145
|
task add "Launch website" "Design mockups" --priority 1
|
|
144
|
-
task add "Launch website" "Set up hosting" --
|
|
146
|
+
task add "Launch website" "Set up hosting" --due 08-15-2026
|
|
145
147
|
|
|
146
148
|
# check your work
|
|
147
149
|
task show "Launch website"
|
|
@@ -15,7 +15,7 @@ and the tree is uncluttered.
|
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
task create "Launch blog"
|
|
18
|
-
task add "Launch blog" "Write outline" --priority 1 --
|
|
18
|
+
task add "Launch blog" "Write outline" --priority 1 --due tomorrow
|
|
19
19
|
task next "Write outline" "Draft intro"
|
|
20
20
|
task show all
|
|
21
21
|
task done "Write outline"
|
|
@@ -25,7 +25,7 @@ task show all
|
|
|
25
25
|
|
|
26
26
|
What this covers, in order:
|
|
27
27
|
1. `create`/`add` — the everything-is-an-item tree, with a couple of fields
|
|
28
|
-
set (`--priority`, `--
|
|
28
|
+
set (`--priority`, `--due tomorrow` — also shows off the relative
|
|
29
29
|
date keyword)
|
|
30
30
|
2. `next` — chains, the newest major feature, right after the tree basics
|
|
31
31
|
3. `show all` — the Rich-rendered tree, chain included (rendered inline
|
|
@@ -19,7 +19,7 @@ Double-click a card to center on it and open its edit panel.
|
|
|
19
19
|
- **Completed** items are tinted green.
|
|
20
20
|
- **`status`** text appears in the top corner of a card (or the color set by
|
|
21
21
|
`STATUS_COLORS` in config, if one's configured for that status).
|
|
22
|
-
- **`
|
|
22
|
+
- **`due`**, if set, appears in the bottom corner.
|
|
23
23
|
- **Soft links** (`task link`) are drawn as a dashed copy of the linked item
|
|
24
24
|
under its host, distinct from the solid parent/child connectors.
|
|
25
25
|
- **Chains** (`task next`/`task chain`) render as a straight bold arrow; a
|
|
@@ -20,13 +20,15 @@ task config pop <key> # remove a key; it returns to its default
|
|
|
20
20
|
|
|
21
21
|
`DISPLAY_STRING`
|
|
22
22
|
: Space-separated list of item attributes shown per row by `task show`.
|
|
23
|
-
Default: `id name status
|
|
23
|
+
Default: `id name status due`.
|
|
24
24
|
|
|
25
25
|
`DISPLAY_COLORS`
|
|
26
26
|
: Space-separated colors, matched position-for-position to `DISPLAY_STRING`.
|
|
27
27
|
Use `_` to leave a column uncolored. Default: `_ white dark_orange _`.
|
|
28
|
+
An item with its own [`color`](item-data.md) set is drawn entirely in that
|
|
29
|
+
color instead, whatever these say.
|
|
28
30
|
|
|
29
31
|
```bash
|
|
30
|
-
task config set DISPLAY_STRING "id name priority
|
|
32
|
+
task config set DISPLAY_STRING "id name priority due"
|
|
31
33
|
task config set DISPLAY_COLORS "_ white cyan _"
|
|
32
34
|
```
|
|
@@ -59,7 +59,7 @@ Every item is a tree node. Start with a root item, then hang children off it.
|
|
|
59
59
|
Names with spaces must be quoted.
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
task create "Launch blog" --
|
|
62
|
+
task create "Launch blog" --due 09-15-2026
|
|
63
63
|
task add "Launch blog" "Write first post" --priority 1
|
|
64
64
|
task add "Launch blog" "Set up hosting"
|
|
65
65
|
task add "Write first post" "Draft outline"
|
|
@@ -15,7 +15,7 @@ A command-line task manager with AI features.
|
|
|
15
15
|
items at any depth; there's no separate notion of "lists"
|
|
16
16
|
- **Attach data to each item** — description, due date, priority, status,
|
|
17
17
|
completion, comments
|
|
18
|
-
- **Filter** — `task show 'priority>=2' '
|
|
18
|
+
- **Filter** — `task show 'priority>=2' 'due<10-01-2026'` narrows the tree to
|
|
19
19
|
matching items and their parents
|
|
20
20
|
- **Soft-link** an item under more than one parent without moving it
|
|
21
21
|
- **Interactive mode** — run `task` with no arguments for a live view that
|
|
@@ -5,25 +5,48 @@ fields. Set them as named options — `--<name> <value>` — on `task create`,
|
|
|
5
5
|
`task add`, and `task update`:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
task create "Ship v2" --
|
|
8
|
+
task create "Ship v2" --due 10-01-2026 --priority 2
|
|
9
9
|
task update "Ship v2" --status "in progress" --description "cut the release branch first"
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
| Option | Value | Meaning |
|
|
13
13
|
|---|---|---|
|
|
14
14
|
| `--description` | text | A longer note on the item. Quote it if it has spaces. |
|
|
15
|
-
| `--
|
|
15
|
+
| `--due` | `MM-DD-YYYY` | When the item is due. |
|
|
16
16
|
| `--priority` | integer | A number you choose; sort or filter by it. Default `0`. |
|
|
17
17
|
| `--status` | text | A free-form state such as `blocked` or `in review`. Shown on the node in the browser view. |
|
|
18
18
|
| `--completed` | `true` / `false` | Whether the item is done. |
|
|
19
|
+
| `--color` | a CSS color, e.g. `"#ff8844"` or `green` | Fills the item's card in the browser view, and draws its whole line in `task show` in that color (overriding the `DISPLAY_COLORS` columns). |
|
|
19
20
|
|
|
20
21
|
## Dedicated commands
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
Every field above also has its own command, so you can skip `update` and the
|
|
24
|
+
`--` for the common case:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
task due 4 tomorrow # same as: task update 4 --due tomorrow
|
|
28
|
+
task priority 4 2
|
|
29
|
+
task status 4 "in review"
|
|
30
|
+
task color 4 "#ff8844"
|
|
31
|
+
task description 4 needs a rewrite # the value needn't be quoted
|
|
32
|
+
task due 4 # no value clears the field
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The value is everything after the item, so multi-word text and dates like
|
|
36
|
+
`task due 4 Dec 31 2026` work without quotes.
|
|
37
|
+
|
|
38
|
+
`task description` takes an `-a` flag to append to the existing description
|
|
39
|
+
(on a new line) instead of replacing it:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
task description 4 first draft outline
|
|
43
|
+
task description 4 -a also cover the hosting options
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A few other changes have their own command too:
|
|
23
47
|
|
|
24
48
|
```bash
|
|
25
49
|
task rename 4 "Ship v2.1"
|
|
26
|
-
task status 4 "in review" # same as --status, as its own verb
|
|
27
50
|
task comment 4 "waiting on QA sign-off"
|
|
28
51
|
task complete 4 # mark item 4 done
|
|
29
52
|
task done 4 # alias for complete
|
|
@@ -21,7 +21,7 @@ already exists — give the parent first, then the new item's name. Both accept
|
|
|
21
21
|
[item data](item-data.md) as named options:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
task add "Launch blog" "Set up hosting" --
|
|
24
|
+
task add "Launch blog" "Set up hosting" --due 09-30-2026 --priority 1
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
## View items
|
|
@@ -40,11 +40,11 @@ the items matching *all* of them, each with its parent chain for context:
|
|
|
40
40
|
```bash
|
|
41
41
|
task show 'name=Write*' # = on text is a glob match
|
|
42
42
|
task show 'priority>=2' 'completed=false'
|
|
43
|
-
task show 'status=in review' '
|
|
43
|
+
task show 'status=in review' 'due<10-01-2026'
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Operators are `=`, `>`, `<`, `>=`, `<=`. Text fields (`name`, `description`,
|
|
47
|
-
`status`) glob-match on `=`; `priority`, `
|
|
47
|
+
`status`) glob-match on `=`; `priority`, `due`, `created_on`, and `id`
|
|
48
48
|
compare numerically / by date. Put no space around the operator, and quote each
|
|
49
49
|
test so your shell doesn't treat `>` and `<` as redirects.
|
|
50
50
|
|
|
@@ -113,9 +113,9 @@ class Controller:
|
|
|
113
113
|
kwargs["completed"] = v
|
|
114
114
|
else:
|
|
115
115
|
kwargs["completed"] = str(v).strip().lower() in ("true", "1", "yes")
|
|
116
|
-
case "
|
|
116
|
+
case "due":
|
|
117
117
|
if isinstance(v, str):
|
|
118
|
-
kwargs["
|
|
118
|
+
kwargs["due"] = parse_date_value(v)
|
|
119
119
|
case "depends_on":
|
|
120
120
|
# CLI-facing alias for the model's dependency_ids field;
|
|
121
121
|
# accepts a comma-separated id list ("1,2,3") or one id
|
|
@@ -207,6 +207,25 @@ class Controller:
|
|
|
207
207
|
|
|
208
208
|
db.commit()
|
|
209
209
|
|
|
210
|
+
def set_attribute(item_id: int|str, attr: str, value: str = "", append: bool = False):
|
|
211
|
+
"""Backs the dedicated `task <attr> {id|name} {value}` commands: set
|
|
212
|
+
one item field. An empty value clears it back to the field's default
|
|
213
|
+
(no due date, priority 0, no color, ...). With `append` (description
|
|
214
|
+
only) the value goes on a new line after the existing text instead of
|
|
215
|
+
replacing it."""
|
|
216
|
+
if append:
|
|
217
|
+
if attr != "description":
|
|
218
|
+
Controller.throw_error(f"-a only applies to description, not {attr}")
|
|
219
|
+
if value == "":
|
|
220
|
+
Controller.throw_error("nothing to append - give the text to add")
|
|
221
|
+
item = Controller._resolve_item(item_id)
|
|
222
|
+
item_id = item.id
|
|
223
|
+
if item.description.strip():
|
|
224
|
+
value = f"{item.description.rstrip()}\n{value}"
|
|
225
|
+
elif value == "":
|
|
226
|
+
value = TodoItem.model_fields[attr].default
|
|
227
|
+
Controller.update_item(item_id, **{attr: value})
|
|
228
|
+
|
|
210
229
|
def delete_item(id_: int|str, chain: bool = False):
|
|
211
230
|
item = Controller._resolve_item(id_)
|
|
212
231
|
|
|
@@ -596,9 +615,20 @@ def execute_commands(*args, **kwargs) -> int:
|
|
|
596
615
|
case "move":
|
|
597
616
|
Controller.move_item(args[1], args[2])
|
|
598
617
|
|
|
599
|
-
case "rename":
|
|
618
|
+
case "rename" | "name":
|
|
600
619
|
Controller.update_item(args[1], name=args[2])
|
|
601
620
|
|
|
621
|
+
# one dedicated command per item field: `task due 10 tomorrow`.
|
|
622
|
+
# The value is everything after the item, so it needn't be quoted
|
|
623
|
+
# (`task description 10 needs a rewrite`); leaving it off clears
|
|
624
|
+
# it, and `description -a` appends to the existing text instead
|
|
625
|
+
case "description" | "due" | "priority" | "color" | "status":
|
|
626
|
+
append = "-a" in args
|
|
627
|
+
words = [a for a in args[1:] if a != "-a"]
|
|
628
|
+
if not words:
|
|
629
|
+
Controller.throw_error(f"usage: task {args[0]} {{id|name}} {{value}}")
|
|
630
|
+
Controller.set_attribute(words[0], args[0], " ".join(words[1:]), append=append)
|
|
631
|
+
|
|
602
632
|
case "pomo":
|
|
603
633
|
pomodoro_service(int(args[1]), int(args[2]))
|
|
604
634
|
|
|
@@ -607,13 +637,6 @@ def execute_commands(*args, **kwargs) -> int:
|
|
|
607
637
|
import orjson as json
|
|
608
638
|
print_json(json.dumps(db.get_item(int(args[1])).model_dump()).decode())
|
|
609
639
|
|
|
610
|
-
case "status":
|
|
611
|
-
if len(args) < 3:
|
|
612
|
-
item_id, status_val = args[1], ""
|
|
613
|
-
else:
|
|
614
|
-
item_id, status_val = args[1:3]
|
|
615
|
-
Controller.update_item(item_id, status=status_val)
|
|
616
|
-
|
|
617
640
|
case "exit" | "exit()" | "quit":
|
|
618
641
|
return 0
|
|
619
642
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Shared due-date parsing: a couple of relative keywords plus a handful of
|
|
2
|
-
common formats, tried in turn. Used by both the CLI's --
|
|
2
|
+
common formats, tried in turn. Used by both the CLI's --due/create/
|
|
3
3
|
update path (cli.py's _parse_item_kwargs) and date-attribute filter values
|
|
4
|
-
(filters.py's
|
|
4
|
+
(filters.py's due/created_on) so the two don't drift apart.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from datetime import datetime, timedelta
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Attribute filters for `task show`.
|
|
3
3
|
|
|
4
|
-
task show status="IN PROGRESS" priority>=2
|
|
4
|
+
task show status="IN PROGRESS" priority>=2 due<12-31-2026
|
|
5
5
|
|
|
6
6
|
Each argument is one filter, `attr<op>value`, with no spaces around the
|
|
7
7
|
operator (a quoted value with spaces still arrives as a single token). Every
|
|
@@ -23,7 +23,7 @@ from taskai.json_dir_database import DatabaseError
|
|
|
23
23
|
|
|
24
24
|
# attr -> how both sides of the comparison should be read
|
|
25
25
|
_INT_ATTRS = {"id", "parent_id", "priority"}
|
|
26
|
-
_DATE_ATTRS = {"
|
|
26
|
+
_DATE_ATTRS = {"due", "created_on"}
|
|
27
27
|
_BOOL_ATTRS = {"completed"}
|
|
28
28
|
_STR_ATTRS = {"name", "description", "status"}
|
|
29
29
|
|
|
@@ -15,8 +15,8 @@ Viewing:
|
|
|
15
15
|
'task show {attr}{op}{value} ...' --> show only items matching every filter, each with its parent chain for context
|
|
16
16
|
ops: = > < >= <= ; text uses = with fnmatch (name=Write*), the rest compare numbers/dates
|
|
17
17
|
no spaces around the operator; quote each expression so your shell doesn't treat > < as redirects
|
|
18
|
-
e.g. task show 'status=IN PROGRESS' 'priority>=2' '
|
|
19
|
-
|
|
18
|
+
e.g. task show 'status=IN PROGRESS' 'priority>=2' 'due<12-31-2026'
|
|
19
|
+
due/created_on also accept the keywords today/tomorrow, e.g. 'due=today', 'due<tomorrow'
|
|
20
20
|
|
|
21
21
|
Creating:
|
|
22
22
|
'task create {name} {--field value ...}' --> create a new top-level (root) item - use this when there's no existing parent to attach to
|
|
@@ -25,7 +25,10 @@ Creating:
|
|
|
25
25
|
Modifying items (an id or a name works everywhere):
|
|
26
26
|
'task update {id|name} {--field value ...}' --> update fields on an item
|
|
27
27
|
'task rename {id|name} {new name}' --> rename an item
|
|
28
|
-
'task
|
|
28
|
+
'task {field} {id|name} {value}' --> set one item field with its own command; field is one of description, due, priority, status, color
|
|
29
|
+
e.g. task due 10 tomorrow / task priority 10 2 / task status 10 "in review" / task color 10 "#ff8844"
|
|
30
|
+
the value needn't be quoted (task description 10 needs a rewrite); leave it off to clear the field (task due 10)
|
|
31
|
+
task description {id|name} -a {text} --> append text to the existing description, on a new line, instead of replacing it
|
|
29
32
|
'task comment {id|name} {text}' --> add a comment to an item
|
|
30
33
|
'task complete {id|name}' / 'task done {id|name}' --> mark an item complete; add -r (or -recursive) to also complete all its descendants
|
|
31
34
|
'task undone {id|name}' --> mark an item not complete; add -r (or -recursive) to also un-complete all its descendants
|
|
@@ -50,10 +53,11 @@ Deleting:
|
|
|
50
53
|
|
|
51
54
|
Item fields (pass as named options, e.g. --priority 2, to create/add/update):
|
|
52
55
|
description string
|
|
53
|
-
|
|
56
|
+
due MM-DD-YYYY, YYYY-MM-DD, MM/DD/YYYY, "Dec 31, 2026", or the keywords today/tomorrow
|
|
54
57
|
priority integer
|
|
55
58
|
status string
|
|
56
59
|
completed true|false
|
|
60
|
+
color a CSS color such as #ff8844 or green - fills the item's card in the browser and colors its whole line in `task show`
|
|
57
61
|
|
|
58
62
|
AI:
|
|
59
63
|
'task ai {prompt}' --> feed a prompt to an LLM, which converts it into a series of the commands above and runs them
|
|
@@ -84,7 +88,7 @@ help_examples = """
|
|
|
84
88
|
Worked examples - copy/paste and adapt. Names with spaces must be quoted.
|
|
85
89
|
|
|
86
90
|
Build a small project tree:
|
|
87
|
-
task create "Launch blog" --
|
|
91
|
+
task create "Launch blog" --due 09-15-2026
|
|
88
92
|
task add "Launch blog" "Write first post" --priority 1
|
|
89
93
|
task add "Launch blog" "Set up hosting"
|
|
90
94
|
task add "Write first post" "Draft outline"
|
|
@@ -92,6 +96,10 @@ Build a small project tree:
|
|
|
92
96
|
|
|
93
97
|
Work an item and finish it:
|
|
94
98
|
task status 4 "in progress"
|
|
99
|
+
task due 4 tomorrow # every item field has a command like this
|
|
100
|
+
task priority 4 2
|
|
101
|
+
task description 4 first draft outline
|
|
102
|
+
task description 4 -a "also cover the hosting options" # -a appends instead of replacing
|
|
95
103
|
task comment 4 "stuck on the intro paragraph"
|
|
96
104
|
task done 4 # completes just item 4
|
|
97
105
|
task done 4 -r # completes item 4 and everything under it
|
|
@@ -23,6 +23,30 @@ def _is_chain_head(item: TodoItem) -> bool:
|
|
|
23
23
|
return item.prev_chain_id is None and item.next_chain_id is not None
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def _migrate_legacy_keys(user_data: UserData) -> None:
|
|
27
|
+
"""Backwards compatibility for data written before `due_by` became `due`.
|
|
28
|
+
|
|
29
|
+
Runs once per load, right after the file is read, so everything
|
|
30
|
+
downstream - raw-dict reads like get_item_attr included - only ever sees
|
|
31
|
+
the current names; the next commit writes them back out under the new
|
|
32
|
+
ones. A legacy `due_by` only fills in `due` when it has a value and `due`
|
|
33
|
+
doesn't, and the legacy key is dropped either way so it can't linger.
|
|
34
|
+
"""
|
|
35
|
+
for item in user_data.todo_items.values():
|
|
36
|
+
if "due_by" in item:
|
|
37
|
+
legacy_due = item.pop("due_by")
|
|
38
|
+
if legacy_due and not item.get("due"):
|
|
39
|
+
item["due"] = legacy_due
|
|
40
|
+
|
|
41
|
+
# a saved DISPLAY_STRING can name the old attribute too (the default
|
|
42
|
+
# used to, and `task config set` persists every default it fills in)
|
|
43
|
+
display_string = user_data.config.get("DISPLAY_STRING")
|
|
44
|
+
if display_string:
|
|
45
|
+
user_data.config["DISPLAY_STRING"] = " ".join(
|
|
46
|
+
"due" if attr == "due_by" else attr for attr in display_string.split(" ")
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
26
50
|
class JsonDirectoryDatabase:
|
|
27
51
|
"""
|
|
28
52
|
All views extracted from the database are read only
|
|
@@ -59,6 +83,7 @@ class JsonDirectoryDatabase:
|
|
|
59
83
|
)
|
|
60
84
|
else:
|
|
61
85
|
self.user_data = UserData(**json.loads(json_bstring))
|
|
86
|
+
_migrate_legacy_keys(self.user_data)
|
|
62
87
|
else:
|
|
63
88
|
raise DatabaseError("Database already connected")
|
|
64
89
|
|
|
@@ -36,7 +36,7 @@ class TodoItem(Base):
|
|
|
36
36
|
created_on: datetime = Field(default_factory=datetime.now)
|
|
37
37
|
completed: bool = False
|
|
38
38
|
description: str = ""
|
|
39
|
-
|
|
39
|
+
due: Optional[datetime] = None
|
|
40
40
|
comment_ids: list[int] = Field(default_factory=list)
|
|
41
41
|
dependency_ids: list[int] = Field(default_factory=list)
|
|
42
42
|
linked_ids: list[int] = Field(default_factory=list)
|
|
@@ -73,7 +73,7 @@ class CLIConfig(Base):
|
|
|
73
73
|
AI_MODEL: Optional[str] = None
|
|
74
74
|
|
|
75
75
|
# VIEW
|
|
76
|
-
DISPLAY_STRING: str = "id name status
|
|
76
|
+
DISPLAY_STRING: str = "id name status due"
|
|
77
77
|
DISPLAY_COLORS: str = "_ white dark_orange _"
|
|
78
78
|
# per-status-string color for the DAG node's status label, e.g.
|
|
79
79
|
# "RUNNING=green,BLOCKED=red" - browser-only today (CLI status display
|
|
@@ -25,7 +25,7 @@ function isoToDateInputValue(iso) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// "YYYY-MM-DD" (native <input type="date"> value) -> "MM-DD-YYYY", matching
|
|
28
|
-
// the CLI's documented --
|
|
28
|
+
// the CLI's documented --due format
|
|
29
29
|
function dateInputValueToCliFormat(value) {
|
|
30
30
|
const [year, month, day] = value.split("-");
|
|
31
31
|
return `${month}-${day}-${year}`;
|
|
@@ -42,7 +42,7 @@ const FIELD_DEFS = [
|
|
|
42
42
|
// but an empty string here correctly means "no override, use the default"
|
|
43
43
|
{ key: "color", label: "Color", type: "text" },
|
|
44
44
|
{ key: "priority", label: "Priority", type: "number" },
|
|
45
|
-
{ key: "
|
|
45
|
+
{ key: "due", label: "Due", type: "date", format: isoToDateInputValue },
|
|
46
46
|
{ key: "completed", label: "Completed", type: "checkbox" },
|
|
47
47
|
// description goes last and is flagged `grow`: it's the big free-text
|
|
48
48
|
// field, so it sits at the bottom and stretches to fill whatever panel
|
|
@@ -102,8 +102,8 @@ function handleFieldChange(item, def, value, immediate = false) {
|
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
if (def.key === "
|
|
106
|
-
if (!value) return; // clearing
|
|
105
|
+
if (def.key === "due") {
|
|
106
|
+
if (!value) return; // clearing due isn't supported by the CLI today
|
|
107
107
|
value = dateInputValueToCliFormat(value);
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
// direction key moves to. Every direction has exactly one meaning, with no
|
|
3
3
|
// fallbacks:
|
|
4
4
|
// - a plain (non-chain) node: down/up are children/parent, as ever
|
|
5
|
-
// - a chain member: down
|
|
5
|
+
// - a chain member: down is chainNext, up is chainPrev — the chain is the
|
|
6
6
|
// spine you walk, and its side subtrees are never entered by accident.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// A chain's head has no chainPrev, so up from it leaves the chain for the
|
|
8
|
+
// head's tree parent (the synthetic root, i.e. show all, for a top-level
|
|
9
|
+
// chain)
|
|
9
10
|
// - into: chain member -> its subtree (remembered/first child), else nothing
|
|
10
11
|
// - out: jump to the nearest chain member whose subtree contains the
|
|
11
12
|
// current node (starting from the head of the node's own chain, if it's
|
|
@@ -49,10 +50,11 @@ function navigateGraph(direction, cur, graph) {
|
|
|
49
50
|
if (direction === "down") {
|
|
50
51
|
target = inChain ? cur.chainNext : rememberedChild(cur);
|
|
51
52
|
} else if (direction === "up") {
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
// a node has either a chainPrev (a non-head chain member) or a tree
|
|
54
|
+
// parent (a chain's head, or any plain node), never both, so this never
|
|
55
|
+
// has to choose: walk back along the chain, and from its head leave it
|
|
56
|
+
// for the tree parent (the synthetic root for a top-level chain)
|
|
57
|
+
target = cur.chainPrev || cur.parent;
|
|
56
58
|
} else if (direction === "into") {
|
|
57
59
|
target = inChain ? rememberedChild(cur) : null;
|
|
58
60
|
} else if (direction === "out") {
|
|
@@ -238,13 +238,13 @@ function drawNode(ctx, node, isHovered, isSelected) {
|
|
|
238
238
|
ctx.fillText(statusLabel, nodeX + node.size - STYLE.node.idPadding, nodeY + STYLE.node.idPadding);
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
if (!isShadow && node.
|
|
241
|
+
if (!isShadow && node.due) {
|
|
242
242
|
ctx.fillStyle = STYLE.colors.dueText;
|
|
243
243
|
ctx.font = STYLE.node.idFont;
|
|
244
244
|
ctx.textAlign = "right";
|
|
245
245
|
ctx.textBaseline = "bottom";
|
|
246
246
|
// "2026-12-31T00:00:00" -> "12/31" - compact, locale-agnostic
|
|
247
|
-
const dueLabel = node.
|
|
247
|
+
const dueLabel = node.due.slice(5, 7) + "/" + node.due.slice(8, 10);
|
|
248
248
|
ctx.fillText(dueLabel, nodeX + node.size - STYLE.node.idPadding, nodeY + node.size - STYLE.node.idPadding);
|
|
249
249
|
}
|
|
250
250
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# standard lib
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
1
4
|
# local
|
|
2
5
|
from taskai.json_dir_database import JsonDirectoryDatabase, DatabaseError
|
|
3
6
|
from taskai.models import TodoItem, Comment
|
|
@@ -5,6 +8,7 @@ from taskai.config import config
|
|
|
5
8
|
|
|
6
9
|
# external
|
|
7
10
|
from rich import print
|
|
11
|
+
from rich.color import Color, ColorParseError
|
|
8
12
|
from rich.console import Console
|
|
9
13
|
|
|
10
14
|
|
|
@@ -26,7 +30,7 @@ VALID_ATTRS = {
|
|
|
26
30
|
"id",
|
|
27
31
|
"name",
|
|
28
32
|
"created_on",
|
|
29
|
-
"
|
|
33
|
+
"due",
|
|
30
34
|
"priority",
|
|
31
35
|
"status"
|
|
32
36
|
}
|
|
@@ -61,13 +65,16 @@ def view_lists(
|
|
|
61
65
|
|
|
62
66
|
def _render_display_string(item: TodoItem):
|
|
63
67
|
display_string = ""
|
|
68
|
+
item_color = _rich_color(item.color)
|
|
64
69
|
for i, attr in enumerate(attrs):
|
|
65
70
|
part = getattr(item, attr)
|
|
66
71
|
if not part:
|
|
67
72
|
continue
|
|
68
|
-
part = str(part)
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
part = _format_date(part) if isinstance(part, datetime) else str(part)
|
|
74
|
+
# an item's own color beats the column colors: it's drawn entirely in it
|
|
75
|
+
color = item_color or (colors[i] if display_colors and colors[i] != "_" else None)
|
|
76
|
+
if color:
|
|
77
|
+
part = _wrap_string(part, f"[{color}]", f"[/{color}]")
|
|
71
78
|
display_string += f" {part}"
|
|
72
79
|
display_string = _wrap_string(display_string, "[strike]", "[/strike]", condition=item.completed)
|
|
73
80
|
return display_string
|
|
@@ -131,9 +138,11 @@ def view_item(
|
|
|
131
138
|
item = db.get_item(item_id)
|
|
132
139
|
|
|
133
140
|
console = Console()
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
item_color = _rich_color(item.color)
|
|
142
|
+
name = _wrap_string(item.name, f"[{item_color}]", f"[/{item_color}]", condition=bool(item_color))
|
|
143
|
+
console.print(f"[bold green]Name:[/bold green] {name}")
|
|
144
|
+
if item.due:
|
|
145
|
+
console.print(f"[bold green]Due:[/bold green] {_format_date(item.due)}")
|
|
137
146
|
|
|
138
147
|
if item.description:
|
|
139
148
|
console.print(f"\n[bold green]Description:[/bold green]\n{item.description}")
|
|
@@ -142,7 +151,7 @@ def view_item(
|
|
|
142
151
|
console.print("\n[bold green]\nComments:[/bold green]")
|
|
143
152
|
for comment_id in item.comment_ids:
|
|
144
153
|
comment: Comment = db.get_comment(comment_id)
|
|
145
|
-
console.print(f"{comment.created_on
|
|
154
|
+
console.print(f"{_format_date(comment.created_on)} - {comment.content}")
|
|
146
155
|
|
|
147
156
|
if item.child_ids:
|
|
148
157
|
console.print("\n[bold green]\nSubtasks:[/bold green]")
|
|
@@ -158,6 +167,40 @@ def view_item(
|
|
|
158
167
|
|
|
159
168
|
### Utils
|
|
160
169
|
|
|
170
|
+
# CSS color names Rich has no name for (its named colors come from the 256-color
|
|
171
|
+
# ANSI palette), so an item colored "orange" in the browser can still be shown
|
|
172
|
+
_CSS_COLORS_MISSING_IN_RICH = {
|
|
173
|
+
"orange": "#ffa500", "pink": "#ffc0cb", "gold": "#ffd700", "teal": "#008080",
|
|
174
|
+
"brown": "#a52a2a", "indigo": "#4b0082", "violet": "#ee82ee", "crimson": "#dc143c",
|
|
175
|
+
"coral": "#ff7f50", "salmon": "#fa8072", "lime": "#00ff00", "olive": "#808000",
|
|
176
|
+
"navy": "#000080", "maroon": "#800000", "silver": "#c0c0c0", "gray": "#808080",
|
|
177
|
+
"grey": "#808080",
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _rich_color(css_color: str | None) -> str | None:
|
|
182
|
+
"""An item's `color` (a CSS color string, e.g. "#ff8844" or "green") as a
|
|
183
|
+
Rich color, or None when it's unset or isn't one Rich can draw - an
|
|
184
|
+
unusable color just isn't shown rather than breaking the whole listing."""
|
|
185
|
+
if not css_color:
|
|
186
|
+
return None
|
|
187
|
+
css_color = css_color.strip().lower()
|
|
188
|
+
for candidate in (css_color, _CSS_COLORS_MISSING_IN_RICH.get(css_color)):
|
|
189
|
+
if not candidate:
|
|
190
|
+
continue
|
|
191
|
+
try:
|
|
192
|
+
Color.parse(candidate)
|
|
193
|
+
except ColorParseError:
|
|
194
|
+
continue
|
|
195
|
+
return candidate
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _format_date(value: datetime) -> str:
|
|
200
|
+
"""How every date is shown in the CLI: just the day, MM/DD/YY - the time
|
|
201
|
+
of day is never displayed."""
|
|
202
|
+
return value.strftime("%m/%d/%y")
|
|
203
|
+
|
|
161
204
|
|
|
162
205
|
def _wrap_string(string_, before, after=None, condition=True):
|
|
163
206
|
if after is None:
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import sys, os; sys.path.append(os.getcwd())
|
|
2
|
+
import shutil
|
|
3
|
+
|
|
4
|
+
import orjson as json
|
|
5
|
+
|
|
6
|
+
from taskai.json_dir_database import JsonDirectoryDatabase
|
|
7
|
+
|
|
8
|
+
LEGACY_DIR = "_tmp_legacy_dir"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _write_legacy_file(items: dict, config: dict | None = None):
|
|
12
|
+
os.makedirs(LEGACY_DIR, exist_ok=True)
|
|
13
|
+
data = {"name": "test_user", "id_counter": len(items), "todo_items": items, "config": config or {}}
|
|
14
|
+
with open(os.path.join(LEGACY_DIR, "test_user.json"), "wb") as f:
|
|
15
|
+
f.write(json.dumps(data))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _connect() -> JsonDirectoryDatabase:
|
|
19
|
+
db = JsonDirectoryDatabase(LEGACY_DIR, "test_user")
|
|
20
|
+
db.connect()
|
|
21
|
+
return db
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _cleanup():
|
|
25
|
+
if os.path.exists(LEGACY_DIR):
|
|
26
|
+
shutil.rmtree(LEGACY_DIR)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_due_by_is_read_as_due():
|
|
30
|
+
_cleanup()
|
|
31
|
+
try:
|
|
32
|
+
_write_legacy_file({
|
|
33
|
+
"1": {"id": 1, "name": "old", "due_by": "2026-10-01T00:00:00"},
|
|
34
|
+
"2": {"id": 2, "name": "no due", "due_by": None},
|
|
35
|
+
"3": {"id": 3, "name": "both", "due_by": "2026-01-01T00:00:00", "due": "2026-02-02T00:00:00"},
|
|
36
|
+
})
|
|
37
|
+
db = _connect()
|
|
38
|
+
|
|
39
|
+
assert db.get_item(1).due.isoformat() == "2026-10-01T00:00:00"
|
|
40
|
+
assert db.get_item(2).due is None
|
|
41
|
+
# `due` wins when both are set - the legacy key never overwrites it
|
|
42
|
+
assert db.get_item(3).due.isoformat() == "2026-02-02T00:00:00"
|
|
43
|
+
|
|
44
|
+
# raw-dict reads see the new name too, and the legacy key is gone
|
|
45
|
+
assert db.get_item_attr(1, "due") == "2026-10-01T00:00:00"
|
|
46
|
+
assert all("due_by" not in item for item in db.user_data.todo_items.values())
|
|
47
|
+
finally:
|
|
48
|
+
_cleanup()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_migrated_data_is_written_back_under_the_new_name():
|
|
52
|
+
_cleanup()
|
|
53
|
+
try:
|
|
54
|
+
_write_legacy_file({"1": {"id": 1, "name": "old", "due_by": "2026-10-01T00:00:00"}})
|
|
55
|
+
db = _connect()
|
|
56
|
+
db.commit()
|
|
57
|
+
|
|
58
|
+
with open(os.path.join(LEGACY_DIR, "test_user.json"), "rb") as f:
|
|
59
|
+
saved = json.loads(f.read())
|
|
60
|
+
assert "due_by" not in saved["todo_items"]["1"]
|
|
61
|
+
assert saved["todo_items"]["1"]["due"] == "2026-10-01T00:00:00"
|
|
62
|
+
finally:
|
|
63
|
+
_cleanup()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_saved_display_string_is_migrated():
|
|
67
|
+
_cleanup()
|
|
68
|
+
try:
|
|
69
|
+
_write_legacy_file({}, config={"DISPLAY_STRING": "id name status due_by"})
|
|
70
|
+
db = _connect()
|
|
71
|
+
assert db.get_config().DISPLAY_STRING == "id name status due"
|
|
72
|
+
finally:
|
|
73
|
+
_cleanup()
|
|
@@ -24,7 +24,7 @@ def _populate_db():
|
|
|
24
24
|
child1 = db.create_item(name="child 1", parent_id=root1, priority=3)
|
|
25
25
|
root2 = db.create_item(name="root 2", description="Describe Marcellus Wallace")
|
|
26
26
|
child2 = db.create_item(name="child 2", parent_id=root2, priority=2)
|
|
27
|
-
child3 = db.create_item(name="child 3", parent_id=root2,
|
|
27
|
+
child3 = db.create_item(name="child 3", parent_id=root2, due=datetime(2026,10, 1))
|
|
28
28
|
gchild1 = db.create_item(name="grandchild 1", parent_id=child3, completed=True, description="Say 'what' again.")
|
|
29
29
|
db.create_comment("I dare you", item_id=gchild1)
|
|
30
30
|
db.create_comment("I double dare you mothafucka, say 'what' one more goddamn time", item_id=gchild1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|