simple-project-tracker 1.2.0__tar.gz → 1.2.1__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.
Files changed (82) hide show
  1. {simple_project_tracker-1.2.0/simple_project_tracker.egg-info → simple_project_tracker-1.2.1}/PKG-INFO +3 -1
  2. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/README.md +2 -0
  3. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/pyproject.toml +1 -1
  4. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1/simple_project_tracker.egg-info}/PKG-INFO +3 -1
  5. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_todos.py +42 -0
  6. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/cli/entries.py +5 -5
  7. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/cli/todo.py +69 -7
  8. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/share/help.txt +4 -1
  9. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/LICENSE +0 -0
  10. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/setup.cfg +0 -0
  11. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/simple_project_tracker.egg-info/SOURCES.txt +0 -0
  12. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/simple_project_tracker.egg-info/dependency_links.txt +0 -0
  13. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/simple_project_tracker.egg-info/entry_points.txt +0 -0
  14. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/simple_project_tracker.egg-info/top_level.txt +0 -0
  15. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_cli.py +0 -0
  16. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_commands.py +0 -0
  17. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_daemon.py +0 -0
  18. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_details.py +0 -0
  19. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_doctor.py +0 -0
  20. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_frecency.py +0 -0
  21. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_help.py +0 -0
  22. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_identity.py +0 -0
  23. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_keys.py +0 -0
  24. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_labels.py +0 -0
  25. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_moves.py +0 -0
  26. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_names.py +0 -0
  27. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_paths.py +0 -0
  28. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_portable.py +0 -0
  29. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_relocate.py +0 -0
  30. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_render.py +0 -0
  31. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_selection.py +0 -0
  32. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_settings.py +0 -0
  33. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_storage.py +0 -0
  34. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_undo.py +0 -0
  35. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tests/test_view.py +0 -0
  36. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/__init__.py +0 -0
  37. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/__main__.py +0 -0
  38. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/cli/__init__.py +0 -0
  39. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/cli/app.py +0 -0
  40. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/cli/commands.py +0 -0
  41. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/cli/doctor.py +0 -0
  42. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/cli/relocate.py +0 -0
  43. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/config/__init__.py +0 -0
  44. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/config/defaults.py +0 -0
  45. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/config/keys.py +0 -0
  46. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/config/metadata.py +0 -0
  47. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/config/paths.py +0 -0
  48. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/config/settings.py +0 -0
  49. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/config/writer.py +0 -0
  50. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/core/__init__.py +0 -0
  51. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/core/daemon.py +0 -0
  52. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/core/discovery.py +0 -0
  53. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/core/entries.py +0 -0
  54. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/core/frecency.py +0 -0
  55. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/core/identity.py +0 -0
  56. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/core/inspect.py +0 -0
  57. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/core/labels.py +0 -0
  58. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/core/models.py +0 -0
  59. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/core/portable.py +0 -0
  60. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/core/selection.py +0 -0
  61. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/core/storage.py +0 -0
  62. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/core/todos.py +0 -0
  63. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/core/undo.py +0 -0
  64. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/core/view.py +0 -0
  65. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/py.typed +0 -0
  66. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/share/completion.bash +0 -0
  67. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/share/completion.fish +0 -0
  68. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/share/completion.zsh +0 -0
  69. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/share/settings.toml +0 -0
  70. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/ui/__init__.py +0 -0
  71. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/ui/ansi.py +0 -0
  72. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/ui/ask.py +0 -0
  73. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/ui/details.py +0 -0
  74. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/ui/help.py +0 -0
  75. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/ui/mentions.py +0 -0
  76. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/ui/render.py +0 -0
  77. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/ui/todos.py +0 -0
  78. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/util/__init__.py +0 -0
  79. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/util/compat.py +0 -0
  80. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/util/files.py +0 -0
  81. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/util/text.py +0 -0
  82. {simple_project_tracker-1.2.0 → simple_project_tracker-1.2.1}/tracker/util/unknown.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simple-project-tracker
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Simple project tracker
5
5
  Author-email: MattFor <mattfor@relaxy.xyz>
6
6
  License-Expression: MIT
@@ -120,9 +120,11 @@ List of notes that's separate from the main project database.
120
120
  ```sh
121
121
  t td # the whole list
122
122
  t td add write the changelog # the rest of the line is the name
123
+ t td add milk s shop n oat # with a status and a note up front
123
124
  t td 3 done # also: t td done 3
124
125
  t td 3 st blocked # any status you like
125
126
  t td 3 n waiting on review # and a note under it
127
+ t td write the docs n today # by name; offers to add one that is not there
126
128
  t td list s:todo # only what is still open
127
129
  t td clear # drop every finished one
128
130
  t td undo # take the last change back
@@ -88,9 +88,11 @@ List of notes that's separate from the main project database.
88
88
  ```sh
89
89
  t td # the whole list
90
90
  t td add write the changelog # the rest of the line is the name
91
+ t td add milk s shop n oat # with a status and a note up front
91
92
  t td 3 done # also: t td done 3
92
93
  t td 3 st blocked # any status you like
93
94
  t td 3 n waiting on review # and a note under it
95
+ t td write the docs n today # by name; offers to add one that is not there
94
96
  t td list s:todo # only what is still open
95
97
  t td clear # drop every finished one
96
98
  t td undo # take the last change back
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simple-project-tracker"
7
- version = "1.2.0"
7
+ version = "1.2.1"
8
8
  description = "Simple project tracker"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simple-project-tracker
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Simple project tracker
5
5
  Author-email: MattFor <mattfor@relaxy.xyz>
6
6
  License-Expression: MIT
@@ -120,9 +120,11 @@ List of notes that's separate from the main project database.
120
120
  ```sh
121
121
  t td # the whole list
122
122
  t td add write the changelog # the rest of the line is the name
123
+ t td add milk s shop n oat # with a status and a note up front
123
124
  t td 3 done # also: t td done 3
124
125
  t td 3 st blocked # any status you like
125
126
  t td 3 n waiting on review # and a note under it
127
+ t td write the docs n today # by name; offers to add one that is not there
126
128
  t td list s:todo # only what is still open
127
129
  t td clear # drop every finished one
128
130
  t td undo # take the last change back
@@ -150,6 +150,48 @@ def test_a_todo_may_come_before_its_action(tmp_path: Path):
150
150
  assert status_of(load_todos(holder.settings)["1"]) == "blocked"
151
151
 
152
152
 
153
+ def test_every_word_before_the_action_is_the_todo(tmp_path: Path):
154
+ holder = context(tmp_path)
155
+
156
+ _ = run(holder, "add", "read the manual")
157
+ _ = run(holder, "add", "write the docs")
158
+
159
+ output = run(holder, "write", "the", "docs", "n", "before", "friday")
160
+
161
+ assert "note did nothing" not in output
162
+ assert load_todos(holder.settings)["2"].get("note") == "before friday"
163
+
164
+
165
+ def test_a_todo_is_added_with_its_note_and_status(tmp_path: Path):
166
+ holder = context(tmp_path)
167
+
168
+ _ = run(holder, "add", "buy", "milk", "s", "blocked", "n", "oat", "not", "dairy")
169
+
170
+ todo = load_todos(holder.settings)["1"]
171
+
172
+ assert todo["name"] == "buy milk"
173
+ assert status_of(todo) == "blocked"
174
+ assert todo.get("note") == "oat not dairy"
175
+
176
+
177
+ def test_a_note_for_a_missing_todo_adds_it(tmp_path: Path):
178
+ holder = context(tmp_path)
179
+
180
+ _ = run(holder, "add", "read the manual")
181
+ _ = run(holder, "lexiglance", "n", "respond", "to", "the", "pr")
182
+
183
+ todo = load_todos(holder.settings)["2"]
184
+
185
+ assert todo["name"] == "lexiglance"
186
+ assert todo.get("note") == "respond to the pr"
187
+
188
+ _ = run(holder, "9", "n", "nothing")
189
+ _ = run(holder, "manual", "n", "just the note")
190
+
191
+ assert len(load_todos(holder.settings)) == 2
192
+ assert load_todos(holder.settings)["1"].get("note") == "just the note"
193
+
194
+
153
195
  def test_a_bare_note_clears_it(tmp_path: Path):
154
196
  holder = context(tmp_path)
155
197
 
@@ -424,16 +424,16 @@ class Kind:
424
424
 
425
425
  return wanted
426
426
 
427
- def edit(self, context: Context, args: list[str]) -> int:
427
+ def edit(self, context: Context, args: list[str], command: str = "edit") -> int:
428
428
  if not args:
429
- return self.missing("edit")
429
+ return self.missing(command)
430
430
 
431
431
  if len(args) < 2:
432
432
  print(f"[ERROR] edit requires a field: {', '.join(self.editable)}")
433
433
  return 1
434
434
 
435
435
  entries = self.load(context)
436
- selected = self.chosen(context, entries, [args[0]], "edit")
436
+ selected = self.chosen(context, entries, [args[0]], command)
437
437
 
438
438
  if selected is None:
439
439
  return 1
@@ -514,13 +514,13 @@ class Kind:
514
514
  return self.missing(name)
515
515
 
516
516
  if name in self.greedy:
517
- return self.edit(context, [args[0], name, *args[1:]])
517
+ return self.edit(context, [args[0], name, *args[1:]], name)
518
518
 
519
519
  if len(args) < 2:
520
520
  print(f"[ERROR] {name} requires a value")
521
521
  return 1
522
522
 
523
- return self.edit(context, [args[0], name, " ".join(args[1:])])
523
+ return self.edit(context, [args[0], name, " ".join(args[1:])], name)
524
524
 
525
525
  def statuses(self, context: Context) -> int:
526
526
  settings = context.settings
@@ -1,3 +1,4 @@
1
+ import sys
1
2
  from typing import Any, final
2
3
 
3
4
  from tracker.cli.entries import (
@@ -31,6 +32,7 @@ from tracker.core.todos import (
31
32
  )
32
33
  from tracker.ui import mentions
33
34
  from tracker.ui.ansi import C
35
+ from tracker.ui.ask import confirm as ask
34
36
  from tracker.ui.help import print_topic
35
37
  from tracker.ui.todos import (
36
38
  note_text,
@@ -95,6 +97,9 @@ TAKES_SELECTION = frozenset(
95
97
  {"check", "done", "edit", "note", "remove", "rename", "reopen", "status"}
96
98
  )
97
99
 
100
+ # The words that end a new todo's name and start its fields, as in add milk n oat
101
+ ADD_FIELDS = frozenset({"n", "nt", "s", "st"})
102
+
98
103
 
99
104
  def action_of(token: str) -> str:
100
105
  return ALIASES.get(token.lower().strip("-"), "")
@@ -209,14 +214,28 @@ def action_list(context: Context, args: list[str]) -> int:
209
214
 
210
215
 
211
216
  def action_add(context: Context, args: list[str]) -> int:
212
- name = " ".join(args).strip()
217
+ at = next(
218
+ (
219
+ index
220
+ for index, token in enumerate(args)
221
+ if token.lower().strip("-") in ADD_FIELDS
222
+ ),
223
+ len(args),
224
+ )
225
+
226
+ name = " ".join(args[:at]).strip()
213
227
 
214
228
  if not name:
215
229
  print("[ERROR] add requires something to do")
216
- print(f"{C.GRAY} t td add read the manual{C.RESET}")
230
+ print(f"{C.GRAY} t td add read the manual n before friday{C.RESET}")
217
231
 
218
232
  return 1
219
233
 
234
+ wanted = TODO.fields_from(args[at:])
235
+
236
+ if wanted is None:
237
+ return 1
238
+
220
239
  settings = context.settings
221
240
  todos = TODO.load(context)
222
241
 
@@ -227,6 +246,9 @@ def action_add(context: Context, args: list[str]) -> int:
227
246
  created=stamp(settings),
228
247
  )
229
248
 
249
+ for field, value in wanted:
250
+ TODO.set(context, todo, field, value)
251
+
230
252
  key = key_of(todo)
231
253
  todos[key] = todo
232
254
 
@@ -325,6 +347,27 @@ def action_undo(context: Context) -> int:
325
347
  return 0
326
348
 
327
349
 
350
+ def offer_new(context: Context, name: str) -> bool:
351
+ """A note or status for a todo that is not there yet may be meant to add it."""
352
+
353
+ # A number, a range or a selector (#3, :2, s:todo, all) names one that should exist
354
+ if (
355
+ name.lower() in ALL_SELECTORS
356
+ or not any(letter.isalpha() for letter in name)
357
+ or any(mark in name for mark in ":=")
358
+ ):
359
+ return False
360
+
361
+ # The same test a name selector makes, so a todo it would find is never doubled
362
+ if any(name.lower() in todo["name"].lower() for todo in TODO.load(context).values()):
363
+ return False
364
+
365
+ if not (context.assume_yes or sys.stdin.isatty()):
366
+ return False
367
+
368
+ return ask(f"no todo matches '{name}', add it?", assume_yes=context.assume_yes)
369
+
370
+
328
371
  #
329
372
  # The command
330
373
  #
@@ -337,12 +380,31 @@ def command_todo(context: Context, args: list[str]) -> int:
337
380
  action = action_of(args[0]) if args else "list"
338
381
  arguments = args[1:]
339
382
 
340
- # A td may come before its action
341
- if not action and len(args) > 1:
342
- following = action_of(args[1])
383
+ # A td may come before its action, and every word up to it is its name
384
+ if not action:
385
+ at = next(
386
+ (
387
+ index
388
+ for index, token in enumerate(args)
389
+ if action_of(token) in TAKES_SELECTION
390
+ ),
391
+ 0,
392
+ )
393
+
394
+ if at:
395
+ action, arguments = (
396
+ action_of(args[at]),
397
+ [" ".join(args[:at]), *args[at + 1 :]],
398
+ )
399
+
400
+ if (
401
+ action in ("note", "status")
402
+ and len(arguments) > 1
403
+ and offer_new(context, arguments[0])
404
+ ):
405
+ mark = "n" if action == "note" else "s"
343
406
 
344
- if following in TAKES_SELECTION:
345
- action, arguments = following, [args[0], *args[2:]]
407
+ return action_add(context, [arguments[0], mark, *arguments[1:]])
346
408
 
347
409
  context.log(f"todos: {len(TODO.load(context))}")
348
410
 
@@ -154,7 +154,7 @@
154
154
 
155
155
  {GRAY}Usage:{RESET}
156
156
  {GREEN}tracker todo{RESET}
157
- {GREEN}tracker todo add <what to do...>{RESET}
157
+ {GREEN}tracker todo add <what to do...> [s <status>] [n <note...>]{RESET}
158
158
  {GREEN}tracker todo done <selection>{RESET}
159
159
  {GREEN}tracker todo reopen <selection>{RESET}
160
160
  {GREEN}tracker todo status <selection> <value>{RESET}
@@ -470,6 +470,7 @@
470
470
 
471
471
  {GREEN}t td{RESET} {GRAY}every todo{RESET}
472
472
  {GREEN}t td add <what to do>{RESET} {GRAY}the rest of the line is the name{RESET}
473
+ {GREEN}t td add milk n oat{RESET} {GRAY}up to {GREEN}n{GRAY} or {GREEN}s{GRAY}, then the note or status{RESET}
473
474
  {GREEN}t td done <selection>{RESET} {GRAY}stamps it and marks it {GREEN}done{RESET}
474
475
  {GREEN}t td reopen <selection>{RESET} {GRAY}puts it back{RESET}
475
476
  {GREEN}t td clear{RESET} {GRAY}removes every finished one{RESET}
@@ -499,6 +500,8 @@
499
500
  {GREEN}t td 3 done{RESET} {GRAY}the same as {GREEN}t td done 3{RESET}
500
501
  {GREEN}t td 3 st blocked{RESET}
501
502
  {GREEN}t td 3 nt {r}broken{/} build{RESET}
503
+ {GREEN}t td the docs n ok{RESET} {GRAY}every word up to the action names the todo{RESET}
504
+ {GREEN}t td milk n oat{RESET} {GRAY}offers to add one when nothing is called that{RESET}
502
505
 
503
506
  {GRAY}Editing takes the same fields and short forms as a project does:{RESET}
504
507