whatnext 2.0__tar.gz → 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 (107) hide show
  1. {whatnext-2.0 → whatnext-2.1}/CHANGELOG.md +9 -0
  2. {whatnext-2.0 → whatnext-2.1}/PKG-INFO +1 -1
  3. {whatnext-2.0 → whatnext-2.1}/docs/index.md +3 -0
  4. whatnext-2.1/docs/sequential.md +26 -0
  5. whatnext-2.1/example/projects/fountain.md +8 -0
  6. whatnext-2.1/tasks/listing.md +11 -0
  7. {whatnext-2.0 → whatnext-2.1}/tasks.md +2 -0
  8. {whatnext-2.0 → whatnext-2.1}/tests/edit.bats +1 -0
  9. whatnext-2.1/tests/headerless/annotation.md +5 -0
  10. whatnext-2.1/tests/headerless/deadline.md +1 -0
  11. {whatnext-2.0 → whatnext-2.1}/tests/list.bats +44 -1
  12. {whatnext-2.0 → whatnext-2.1}/tests/matching.bats +9 -0
  13. whatnext-2.1/tests/queue/all-complete.md +8 -0
  14. whatnext-2.1/tests/queue/blocked.md +9 -0
  15. whatnext-2.1/tests/queue/in-progress.md +11 -0
  16. whatnext-2.1/tests/queue/normal.md +6 -0
  17. whatnext-2.1/tests/queue/prioritised.md +9 -0
  18. whatnext-2.1/tests/queue.bats +53 -0
  19. {whatnext-2.0 → whatnext-2.1}/tests/summary.bats +40 -27
  20. {whatnext-2.0 → whatnext-2.1}/tests/test_file.py +67 -0
  21. {whatnext-2.0 → whatnext-2.1}/whatnext/guide.txt +9 -0
  22. {whatnext-2.0 → whatnext-2.1}/whatnext/models.py +24 -7
  23. {whatnext-2.0 → whatnext-2.1}/whatnext/whatnext.py +48 -4
  24. whatnext-2.0/tests/headerless.md +0 -1
  25. {whatnext-2.0 → whatnext-2.1}/.flake8 +0 -0
  26. {whatnext-2.0 → whatnext-2.1}/.github/dependabot.yml +0 -0
  27. {whatnext-2.0 → whatnext-2.1}/.github/workflows/publish.yml +0 -0
  28. {whatnext-2.0 → whatnext-2.1}/.github/workflows/tests.yml +0 -0
  29. {whatnext-2.0 → whatnext-2.1}/.gitignore +0 -0
  30. {whatnext-2.0 → whatnext-2.1}/.whatnext +0 -0
  31. {whatnext-2.0 → whatnext-2.1}/LICENCE +0 -0
  32. {whatnext-2.0 → whatnext-2.1}/LICENSE +0 -0
  33. {whatnext-2.0 → whatnext-2.1}/Makefile +0 -0
  34. {whatnext-2.0 → whatnext-2.1}/README.md +0 -0
  35. {whatnext-2.0 → whatnext-2.1}/archive/done/tasks.md +0 -0
  36. {whatnext-2.0 → whatnext-2.1}/docs/annotations.md +0 -0
  37. {whatnext-2.0 → whatnext-2.1}/docs/basics.md +0 -0
  38. {whatnext-2.0 → whatnext-2.1}/docs/deadlines.md +0 -0
  39. {whatnext-2.0 → whatnext-2.1}/docs/deferring.md +0 -0
  40. {whatnext-2.0 → whatnext-2.1}/docs/exclusions.md +0 -0
  41. {whatnext-2.0 → whatnext-2.1}/docs/installation.md +0 -0
  42. {whatnext-2.0 → whatnext-2.1}/docs/next.md +0 -0
  43. {whatnext-2.0 → whatnext-2.1}/docs/prioritisation.md +0 -0
  44. {whatnext-2.0 → whatnext-2.1}/docs/usage.md +0 -0
  45. {whatnext-2.0 → whatnext-2.1}/example/archived/projects/tangerine.md +0 -0
  46. {whatnext-2.0 → whatnext-2.1}/example/projects/curtain.md +0 -0
  47. {whatnext-2.0 → whatnext-2.1}/example/projects/harvest.md +0 -0
  48. {whatnext-2.0 → whatnext-2.1}/example/projects/obelisk.md +0 -0
  49. {whatnext-2.0 → whatnext-2.1}/example/projects/tinsel.md +0 -0
  50. {whatnext-2.0 → whatnext-2.1}/example/tasks.md +0 -0
  51. {whatnext-2.0 → whatnext-2.1}/example/template.md +0 -0
  52. {whatnext-2.0 → whatnext-2.1}/pyproject.toml +0 -0
  53. {whatnext-2.0 → whatnext-2.1}/scripts/update-usage-examples.py +0 -0
  54. {whatnext-2.0 → whatnext-2.1}/tasks/deferring.md +0 -0
  55. {whatnext-2.0 → whatnext-2.1}/tasks/documentation.md +0 -0
  56. {whatnext-2.0 → whatnext-2.1}/tasks/next.md +0 -0
  57. {whatnext-2.0 → whatnext-2.1}/tasks/test_refactor.md +0 -0
  58. {whatnext-2.0 → whatnext-2.1}/tasks/version_0.1.md +0 -0
  59. {whatnext-2.0 → whatnext-2.1}/tasks/version_0.2.md +0 -0
  60. {whatnext-2.0 → whatnext-2.1}/tasks/version_0.3.md +0 -0
  61. {whatnext-2.0 → whatnext-2.1}/tasks/version_0.4.md +0 -0
  62. {whatnext-2.0 → whatnext-2.1}/tasks/version_0.5.md +0 -0
  63. {whatnext-2.0 → whatnext-2.1}/tasks/version_1.0.md +0 -0
  64. {whatnext-2.0 → whatnext-2.1}/tasks/version_1.1.md +0 -0
  65. {whatnext-2.0 → whatnext-2.1}/tasks/version_1.2.md +0 -0
  66. {whatnext-2.0 → whatnext-2.1}/tasks/version_1.3.md +0 -0
  67. {whatnext-2.0 → whatnext-2.1}/tests/colour.bats +0 -0
  68. {whatnext-2.0 → whatnext-2.1}/tests/config/.whatnext +0 -0
  69. {whatnext-2.0 → whatnext-2.1}/tests/config/.whatnext.alt +0 -0
  70. {whatnext-2.0 → whatnext-2.1}/tests/config/apples.md +0 -0
  71. {whatnext-2.0 → whatnext-2.1}/tests/config/oranges.md +0 -0
  72. {whatnext-2.0 → whatnext-2.1}/tests/config-external +0 -0
  73. {whatnext-2.0 → whatnext-2.1}/tests/config.bats +0 -0
  74. {whatnext-2.0 → whatnext-2.1}/tests/deadline.bats +0 -0
  75. {whatnext-2.0 → whatnext-2.1}/tests/deferring/bare-after.md +0 -0
  76. {whatnext-2.0 → whatnext-2.1}/tests/deferring/circular-a.md +0 -0
  77. {whatnext-2.0 → whatnext-2.1}/tests/deferring/circular-b.md +0 -0
  78. {whatnext-2.0 → whatnext-2.1}/tests/deferring/deferred.md +0 -0
  79. {whatnext-2.0 → whatnext-2.1}/tests/deferring/duplicate-warnings.md +0 -0
  80. {whatnext-2.0 → whatnext-2.1}/tests/deferring/missing-dep.md +0 -0
  81. {whatnext-2.0 → whatnext-2.1}/tests/deferring/regular.md +0 -0
  82. {whatnext-2.0 → whatnext-2.1}/tests/deferring.bats +0 -0
  83. {whatnext-2.0 → whatnext-2.1}/tests/housekeeping.bats +0 -0
  84. {whatnext-2.0 → whatnext-2.1}/tests/invalid.md +0 -0
  85. {whatnext-2.0 → whatnext-2.1}/tests/next/alternate.md +0 -0
  86. {whatnext-2.0 → whatnext-2.1}/tests/next/append-after.md +0 -0
  87. {whatnext-2.0 → whatnext-2.1}/tests/next/append.md +0 -0
  88. {whatnext-2.0 → whatnext-2.1}/tests/next/empty.md +0 -0
  89. {whatnext-2.0 → whatnext-2.1}/tests/next/insert.md +0 -0
  90. {whatnext-2.0 → whatnext-2.1}/tests/next/projects/alpha/tasks/things.md +0 -0
  91. {whatnext-2.0 → whatnext-2.1}/tests/next/projects/alpha/tasks.md +0 -0
  92. {whatnext-2.0 → whatnext-2.1}/tests/next/projects/beta/things.md +0 -0
  93. {whatnext-2.0 → whatnext-2.1}/tests/next/something.txt +0 -0
  94. {whatnext-2.0 → whatnext-2.1}/tests/next/tasks.md +0 -0
  95. {whatnext-2.0 → whatnext-2.1}/tests/next.bats +0 -0
  96. {whatnext-2.0 → whatnext-2.1}/tests/notnext.bats +0 -0
  97. {whatnext-2.0 → whatnext-2.1}/tests/test_defer.py +0 -0
  98. {whatnext-2.0 → whatnext-2.1}/tests/test_edit.py +0 -0
  99. {whatnext-2.0 → whatnext-2.1}/tests/test_limit.py +0 -0
  100. {whatnext-2.0 → whatnext-2.1}/tests/test_output.py +0 -0
  101. {whatnext-2.0 → whatnext-2.1}/tests/test_summary.py +0 -0
  102. {whatnext-2.0 → whatnext-2.1}/tests/test_task.py +0 -0
  103. {whatnext-2.0 → whatnext-2.1}/tests/usage.bats +0 -0
  104. {whatnext-2.0 → whatnext-2.1}/tests/version.bats +0 -0
  105. {whatnext-2.0 → whatnext-2.1}/whatnext/__init__.py +0 -0
  106. {whatnext-2.0 → whatnext-2.1}/whatnext/next.py +0 -0
  107. {whatnext-2.0 → whatnext-2.1}/whatnext/summary.py +0 -0
@@ -1,6 +1,15 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## [v2.0] - 2026-02-09
5
+
6
+ Added `@queue` directive show only one task at a time, for simple sequential
7
+ work that doesn't need to be split across files using `@after`.
8
+
9
+ **Fixes:**
10
+ - Annotation and priority indicators were not appearing for headerless files
11
+
12
+
4
13
  ## [v2.0] - 2026-01-28
5
14
 
6
15
  **Breaking change:** `whatnext` now ignores tasks and headers within
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: whatnext
3
- Version: 2.0
3
+ Version: 2.1
4
4
  Summary: A lister of tasks found in Markdown files
5
5
  Project-URL: Homepage, https://github.com/norm/whatnext
6
6
  Project-URL: Repository, https://github.com/norm/whatnext
@@ -56,6 +56,9 @@ and sections can have [annotations included](annotations.md).
56
56
  If a task has a dependency on other tasks, such that it cannot be done in any
57
57
  order, you can [defer tasks](deferring.md) until all/some other tasks are complete.
58
58
 
59
+ For sequential work where only one task can be done at a time, you can
60
+ [limit output](sequential.md) to the next open task in the file.
61
+
59
62
  If you need to exclude out some Markdown files for any reason, there are
60
63
  [two ways to do that](exclusions.md).
61
64
 
@@ -0,0 +1,26 @@
1
+ # Sequential tasks
2
+
3
+ For simpler work that is done sequentially, either because they have to be
4
+ done one-after-the-other, or because they represent larger blocks that are
5
+ not broken down into subtasks, add `@queue` to the file:
6
+
7
+ ```markdown
8
+ @queue
9
+
10
+ - [ ] backup production database
11
+ - [ ] run migration to add audit columns
12
+ - [ ] backfill audit data from logs
13
+ ```
14
+
15
+ When querying, [priorities](prioritisation.md) and [deadlines](deadlines.md)
16
+ will apply as usual within the file, and only the most important or
17
+ top-most task is presented. If any tasks are marked as in-progress,
18
+ they will all be shown.
19
+
20
+ ```bash
21
+ (computer)% whatnext
22
+ auditing.md:
23
+ - [ ] backup production database
24
+ ```
25
+
26
+ When querying with `--all` or `--summary` arguments, all tasks are presented.
@@ -0,0 +1,8 @@
1
+ Make the Philosopher's Stone.
2
+
3
+ @queue
4
+
5
+ - [ ] nigredo, the blackening or melanosis
6
+ - [ ] albedo, the whitening or leucosis
7
+ - [ ] citrinitas, the yellowing or xanthosis
8
+ - [ ] rubedo, the reddening, purpling, or iosis
@@ -0,0 +1,11 @@
1
+ **One at a time task visibility.**
2
+ For sequential checklists where you have tasks that can/should only be
3
+ done one at a time, but don't want to make multiple files for one task
4
+ to set up `@after` ordering.
5
+
6
+ List:
7
+ - all `[/]` in-progress tasks (there may be multiple)
8
+ - if none, the highest priority open task
9
+ - if none, the highest priority blocked task
10
+
11
+ - [X] Add `@queue` directive to files, limiting a file's output to one(ish)
@@ -15,3 +15,5 @@ New features go in `./tasks/[feature].md`.
15
15
  > next: error: unrecognized arguments: -e something
16
16
  - [X] headers in fenceblocks are used as headers and should not be -- should whatnext ignore tasks in fenceblocks too?
17
17
  - [X] `--summary --relative` with multiple states uses the same character for the second selected state and the unselected complement
18
+ - [X] annotations in `whatnext` blocks don't appear in output unless preceded by a heading
19
+ - [X] overdue/high/medium/imminent comments are not appearing when there are no headings
@@ -65,6 +65,7 @@ setup() {
65
65
  +6 example/projects/harvest.md
66
66
  +5 example/tasks.md
67
67
  +7 example/projects/curtain.md
68
+ +5 example/projects/fountain.md
68
69
  +3 example/projects/tinsel.md
69
70
  +3 example/archived/projects/tangerine.md
70
71
  EOF
@@ -0,0 +1,5 @@
1
+ ```whatnext
2
+ This annotation should appear even without a heading.
3
+ ```
4
+
5
+ - [ ] a task without heading
@@ -0,0 +1 @@
1
+ - [ ] an overdue task without heading @2020-01-01
@@ -57,6 +57,8 @@ function setup {
57
57
  projects/curtain.md:
58
58
  # Project Curtain / Final bow
59
59
  - [ ] Take a bow
60
+ projects/fountain.md:
61
+ - [ ] nigredo, the blackening or melanosis
60
62
  projects/harvest.md:
61
63
  # Project Harvest
62
64
  - [/] turn compost heap
@@ -71,7 +73,7 @@ function setup {
71
73
  diff -u <(echo "$expected_output") <(echo "$output")
72
74
 
73
75
  # no extraneous blank lines at the end
74
- [ $(WHATNEXT_TODAY=2025-12-25 whatnext 2>/dev/null | wc -l) -eq 56 ]
76
+ [ $(WHATNEXT_TODAY=2025-12-25 whatnext 2>/dev/null | wc -l) -eq 58 ]
75
77
 
76
78
  [ $status -eq 0 ]
77
79
  }
@@ -117,6 +119,9 @@ function setup {
117
119
  projects/curtain.md:
118
120
  # Project Curtain / Final bow
119
121
  - [ ] Take a bow
122
+ projects/fountain.md:
123
+ - [ ] nigredo, the blackening or
124
+ melanosis
120
125
  projects/harvest.md:
121
126
  # Project Harvest
122
127
  - [/] turn compost heap
@@ -179,6 +184,11 @@ function setup {
179
184
  # Project Curtain / Close the theatre
180
185
  - [ ] Escort everyone out
181
186
  - [ ] Shut up shop
187
+ projects/fountain.md:
188
+ - [ ] nigredo, the blackening or melanosis
189
+ - [ ] albedo, the whitening or leucosis
190
+ - [ ] citrinitas, the yellowing or xanthosis
191
+ - [ ] rubedo, the reddening, purpling, or iosis
182
192
  projects/harvest.md:
183
193
  # Project Harvest
184
194
  - [/] turn compost heap
@@ -242,6 +252,8 @@ function setup {
242
252
  # Project Harvest / MEDIUM
243
253
  - [ ] buy copper tape for slugs
244
254
 
255
+ fountain.md:
256
+ - [ ] nigredo, the blackening or melanosis
245
257
  harvest.md:
246
258
  # Project Harvest
247
259
  - [/] turn compost heap
@@ -662,3 +674,34 @@ function setup {
662
674
 
663
675
  [ "$found_different" -eq 1 ]
664
676
  }
677
+
678
+ @test "headerless file shows annotation" {
679
+ run --separate-stderr \
680
+ whatnext \
681
+ ../tests/headerless/annotation.md
682
+
683
+ expected_output=$(sed -e 's/^ //' <<" EOF"
684
+ ../tests/headerless/annotation.md:
685
+ This annotation should appear even without a heading.
686
+ - [ ] a task without heading
687
+ EOF
688
+ )
689
+ diff -u <(echo "$expected_output") <(echo "$output")
690
+ [ $status -eq 0 ]
691
+ }
692
+
693
+ @test "headerless file shows priority" {
694
+ WHATNEXT_TODAY=2025-01-01 \
695
+ run --separate-stderr \
696
+ whatnext \
697
+ ../tests/headerless/deadline.md
698
+
699
+ expected_output=$(sed -e 's/^ //' <<" EOF"
700
+ ../tests/headerless/deadline.md:
701
+ # OVERDUE 5y
702
+ - [ ] an overdue task without heading
703
+ EOF
704
+ )
705
+ diff -u <(echo "$expected_output") <(echo "$output")
706
+ [ $status -eq 0 ]
707
+ }
@@ -161,6 +161,8 @@ function setup {
161
161
  # Project Harvest / MEDIUM
162
162
  - [ ] buy copper tape for slugs
163
163
 
164
+ fountain.md:
165
+ - [ ] nigredo, the blackening or melanosis
164
166
  harvest.md:
165
167
  # Project Harvest
166
168
  - [/] turn compost heap
@@ -224,6 +226,11 @@ function setup {
224
226
  # Project Curtain / Close the theatre
225
227
  - [ ] Escort everyone out
226
228
  - [ ] Shut up shop
229
+ projects/fountain.md:
230
+ - [ ] nigredo, the blackening or melanosis
231
+ - [ ] albedo, the whitening or leucosis
232
+ - [ ] citrinitas, the yellowing or xanthosis
233
+ - [ ] rubedo, the reddening, purpling, or iosis
227
234
  projects/harvest.md:
228
235
  # Project Harvest
229
236
  - [/] turn compost heap
@@ -315,6 +322,8 @@ function setup {
315
322
  # Project Harvest / MEDIUM
316
323
  - [ ] buy copper tape for slugs
317
324
 
325
+ fountain.md:
326
+ - [ ] nigredo, the blackening or melanosis
318
327
  harvest.md:
319
328
  # Project Harvest
320
329
  - [/] turn compost heap
@@ -0,0 +1,8 @@
1
+ @queue
2
+
3
+ ```whatnext
4
+ Nothing shown when all complete.
5
+ ```
6
+
7
+ - [X] done
8
+ - [#] cancelled
@@ -0,0 +1,9 @@
1
+ @queue
2
+
3
+ ```whatnext
4
+ Falls back to blocked when no open tasks.
5
+ ```
6
+
7
+ - [X] completed task
8
+ - [#] cancelled task
9
+ - [<] blocked task
@@ -0,0 +1,11 @@
1
+ @queue
2
+
3
+ ```whatnext
4
+ More than one in-progress tasks shows all.
5
+ ```
6
+
7
+ - [/] first in progress
8
+ - [/] second in progress
9
+ - [ ] first open
10
+ - [ ] second open
11
+ - [ ] third open
@@ -0,0 +1,6 @@
1
+ ```whatnext
2
+ A normal task file with multiple tasks.
3
+ ```
4
+
5
+ - [ ] first task
6
+ - [ ] second task
@@ -0,0 +1,9 @@
1
+ @queue
2
+
3
+ ```whatnext
4
+ Highest priority open task shown.
5
+ ```
6
+
7
+ - [ ] normal priority task
8
+ - [ ] **high priority task**
9
+ - [ ] _medium priority task_
@@ -0,0 +1,53 @@
1
+ bats_require_minimum_version 1.5.0
2
+
3
+ @test "a @queue file shows only one task" {
4
+ WHATNEXT_TODAY=2025-01-01 \
5
+ run --separate-stderr \
6
+ whatnext \
7
+ example/projects/fountain.md
8
+
9
+ expected_output=$(sed -e 's/^ //' <<-'EOF'
10
+ example/projects/fountain.md:
11
+ - [ ] nigredo, the blackening or melanosis
12
+ EOF
13
+ )
14
+ diff -u <(echo "$expected_output") <(echo "$output")
15
+ [ $status -eq 0 ]
16
+ }
17
+
18
+ @test "a directory listing" {
19
+ run --separate-stderr \
20
+ whatnext \
21
+ tests/queue
22
+
23
+ expected_output=$(sed -e 's/^ //' <<-'EOF'
24
+ prioritised.md:
25
+ # HIGH
26
+ Highest priority open task shown.
27
+ - [ ] high priority task
28
+
29
+ blocked.md:
30
+ Falls back to blocked when no open tasks.
31
+ - [<] blocked task
32
+ in-progress.md:
33
+ More than one in-progress tasks shows all.
34
+ - [/] first in progress
35
+ - [/] second in progress
36
+ normal.md:
37
+ A normal task file with multiple tasks.
38
+ - [ ] first task
39
+ - [ ] second task
40
+ EOF
41
+ )
42
+ diff -u <(echo "$expected_output") <(echo "$output")
43
+ [ $status -eq 0 ]
44
+ }
45
+
46
+ @test "queue shows nothing when all complete" {
47
+ run --separate-stderr \
48
+ whatnext \
49
+ tests/queue/all-complete.md
50
+
51
+ [ "$output" = "" ]
52
+ [ $status -eq 0 ]
53
+ }
@@ -11,14 +11,15 @@ function setup {
11
11
  --summary
12
12
 
13
13
  expected_output=$(sed -e 's/^ //' <<" EOF"
14
- B P O
15
- ░░░░░░░░░░░░░░░░░░░░░░░░ 0 0 3 tasks.md
16
- ░░░░░░░░ 0 0 1 projects/curtain.md
17
- ▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0 1 5 projects/harvest.md
18
- ▚▚▚▚▚▚▚▚▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░ 1 1 2 projects/obelisk.md
19
- ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0 0 5 projects/tinsel.md
20
- ──────
21
- 1 2 16 19, of 29 total
14
+ B P O
15
+ ░░░░░░░░░░░░░░░░░░░░░░░░ 0 0 3 tasks.md
16
+ ░░░░░░░░ 0 0 1 projects/curtain.md
17
+ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0 0 4 projects/fountain.md
18
+ ▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0 1 5 projects/harvest.md
19
+ ▚▚▚▚▚▚▚▚▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░ 1 1 2 projects/obelisk.md
20
+ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0 0 5 projects/tinsel.md
21
+ ──────
22
+ 1 2 20 23, of 33 total
22
23
 
23
24
  ▚ Blocked ▓ Partial ░ Open
24
25
  EOF
@@ -38,12 +39,13 @@ function setup {
38
39
  C D B P O
39
40
  ░░░░░░░░░░░░░ 0 0 0 0 3 tasks.md
40
41
  ░░░░░░░░░░░░░░░░░ 0 0 0 0 4 projects/curtain.md
42
+ ░░░░░░░░░░░░░░░░░ 0 0 0 0 4 projects/fountain.md
41
43
  ▚▚▚▚████▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░ 1 1 0 1 5 projects/harvest.md
42
44
  ████▓▓▓▓▒▒▒▒▒░░░░░░░░ 0 1 1 1 2 projects/obelisk.md
43
45
  ░░░░░░░░░░░░░░░░░░░░░ 0 0 0 0 5 projects/tinsel.md
44
46
  ▚▚▚▚█████████████ 1 3 0 0 0 archived/projects/tangerine.md
45
47
  ──────────
46
- 2 5 1 2 19 29, of 29 total
48
+ 2 5 1 2 23 33, of 33 total
47
49
 
48
50
  ▚ Cancelled █ Done ▓ Blocked ▒ Partial ░ Open
49
51
  EOF
@@ -93,12 +95,13 @@ function setup {
93
95
  C D B P O
94
96
  ░░░░ 0 0 0 0 3 tasks.md
95
97
  ░░░░░ 0 0 0 0 4 projects/curtain.md
98
+ ░░░░░ 0 0 0 0 4 projects/fountain.md
96
99
  ▚█▒▒░░░░░░ 1 1 0 1 5 projects/harvest.md
97
100
  █▓▒▒░░ 0 1 1 1 2 projects/obelisk.md
98
101
  ░░░░░░ 0 0 0 0 5 projects/tinsel.md
99
102
  ▚████ 1 3 0 0 0 archived/projects/tangerine.md
100
103
  ──────────
101
- 2 5 1 2 19 29, of 29 total
104
+ 2 5 1 2 23 33, of 33 total
102
105
 
103
106
  ▚ Cancelled █ Done ▓ Blocked ▒ Partial ░ Open
104
107
  EOF
@@ -115,14 +118,15 @@ function setup {
115
118
  --open
116
119
 
117
120
  expected_output=$(sed -e 's/^ //' <<" EOF"
118
- O
119
- ████████████████████████████████ 3 tasks.md
120
- ███████████ 1 projects/curtain.md
121
- █████████████████████████████████████████████████████ 5 projects/harvest.md
122
- █████████████████████ 2 projects/obelisk.md
123
- █████████████████████████████████████████████████████ 5 projects/tinsel.md
124
- ──
125
- 16 16, of 29 total
121
+ O
122
+ ███████████████████████████████ 3 tasks.md
123
+ ██████████ 1 projects/curtain.md
124
+ ██████████████████████████████████████████ 4 projects/fountain.md
125
+ ████████████████████████████████████████████████████ 5 projects/harvest.md
126
+ █████████████████████ 2 projects/obelisk.md
127
+ ████████████████████████████████████████████████████ 5 projects/tinsel.md
128
+ ──
129
+ 20 20, of 33 total
126
130
 
127
131
  █ Open
128
132
  EOF
@@ -143,12 +147,13 @@ function setup {
143
147
  O ~
144
148
  ███████████████ 3 0 tasks.md
145
149
  █████ 1 0 projects/curtain.md
150
+ ████████████████████ 4 0 projects/fountain.md
146
151
  ████████████████████████░░░░░░░░░░░░░░░ 5 3 projects/harvest.md
147
152
  ██████████░░░░░░░░░░░░░░ 2 3 projects/obelisk.md
148
153
  ████████████████████████ 5 0 projects/tinsel.md
149
154
  ░░░░░░░░░░░░░░░░░░░░ 0 4 archived/projects/tangerine.md
150
155
  ─────
151
- 16 10 26, of 29 total
156
+ 20 10 30, of 33 total
152
157
 
153
158
  █ Open ░ (Cancelled/Done/Blocked/Partial)
154
159
  EOF
@@ -169,12 +174,13 @@ function setup {
169
174
  P ~
170
175
  ░░░░░░░░░░░░░░░ 0 3 tasks.md
171
176
  ░░░░░ 0 1 projects/curtain.md
177
+ ░░░░░░░░░░░░░░░░░░░░ 0 4 projects/fountain.md
172
178
  █████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 1 7 projects/harvest.md
173
179
  █████░░░░░░░░░░░░░░░░░░░░ 1 4 projects/obelisk.md
174
180
  ░░░░░░░░░░░░░░░░░░░░░░░░░ 0 5 projects/tinsel.md
175
181
  ░░░░░░░░░░░░░░░░░░░░ 0 4 archived/projects/tangerine.md
176
182
  ────
177
- 2 24 26, of 29 total
183
+ 2 28 30, of 33 total
178
184
 
179
185
  █ Partial ░ (Cancelled/Done/Blocked/Open)
180
186
  EOF
@@ -195,12 +201,13 @@ function setup {
195
201
  B ~
196
202
  ░░░░░░░░░░░░░░░ 0 3 tasks.md
197
203
  ░░░░░ 0 1 projects/curtain.md
204
+ ░░░░░░░░░░░░░░░░░░░░ 0 4 projects/fountain.md
198
205
  ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0 8 projects/harvest.md
199
206
  █████░░░░░░░░░░░░░░░░░░░░ 1 4 projects/obelisk.md
200
207
  ░░░░░░░░░░░░░░░░░░░░░░░░░ 0 5 projects/tinsel.md
201
208
  ░░░░░░░░░░░░░░░░░░░░ 0 4 archived/projects/tangerine.md
202
209
  ────
203
- 1 25 26, of 29 total
210
+ 1 29 30, of 33 total
204
211
 
205
212
  █ Blocked ░ (Cancelled/Done/Partial/Open)
206
213
  EOF
@@ -221,12 +228,13 @@ function setup {
221
228
  D ~
222
229
  ░░░░░░░░░░░░░░░ 0 3 tasks.md
223
230
  ░░░░░ 0 1 projects/curtain.md
231
+ ░░░░░░░░░░░░░░░░░░░░ 0 4 projects/fountain.md
224
232
  █████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 1 7 projects/harvest.md
225
233
  █████░░░░░░░░░░░░░░░░░░░░ 1 4 projects/obelisk.md
226
234
  ░░░░░░░░░░░░░░░░░░░░░░░░░ 0 5 projects/tinsel.md
227
235
  ███████████████░░░░░ 3 1 archived/projects/tangerine.md
228
236
  ────
229
- 5 21 26, of 29 total
237
+ 5 25 30, of 33 total
230
238
 
231
239
  █ Done ░ (Cancelled/Blocked/Partial/Open)
232
240
  EOF
@@ -247,12 +255,13 @@ function setup {
247
255
  C ~
248
256
  ░░░░░░░░░░░░░░░ 0 3 tasks.md
249
257
  ░░░░░ 0 1 projects/curtain.md
258
+ ░░░░░░░░░░░░░░░░░░░░ 0 4 projects/fountain.md
250
259
  █████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 1 7 projects/harvest.md
251
260
  ░░░░░░░░░░░░░░░░░░░░░░░░░ 0 5 projects/obelisk.md
252
261
  ░░░░░░░░░░░░░░░░░░░░░░░░░ 0 5 projects/tinsel.md
253
262
  █████░░░░░░░░░░░░░░░ 1 3 archived/projects/tangerine.md
254
263
  ────
255
- 2 24 26, of 29 total
264
+ 2 28 30, of 33 total
256
265
 
257
266
  █ Cancelled ░ (Done/Blocked/Partial/Open)
258
267
  EOF
@@ -274,12 +283,13 @@ function setup {
274
283
  C O ~
275
284
  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 0 3 0 tasks.md
276
285
  ▓▓▓▓▓ 0 1 0 projects/curtain.md
286
+ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 0 4 0 projects/fountain.md
277
287
  ▚▚▚▚▚▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░ 1 5 2 projects/harvest.md
278
288
  ▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░ 0 2 3 projects/obelisk.md
279
289
  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 0 5 0 projects/tinsel.md
280
290
  ▚▚▚▚▚░░░░░░░░░░░░░░ 1 0 3 archived/projects/tangerine.md
281
291
  ──────
282
- 2 16 8 26, of 29 total
292
+ 2 20 8 30, of 33 total
283
293
 
284
294
  ▚ Cancelled ▓ Open ░ (Done/Blocked/Partial)
285
295
  EOF
@@ -300,12 +310,13 @@ function setup {
300
310
  H ~
301
311
  ░░░░░░░░░░░░░░░ 0 3 tasks.md
302
312
  ░░░░░ 0 1 projects/curtain.md
313
+ ░░░░░░░░░░░░░░░░░░░░ 0 4 projects/fountain.md
303
314
  ███████████████░░░░░░░░░░░░░░░░░░░░░░░░░ 3 5 projects/harvest.md
304
315
  ░░░░░░░░░░░░░░░░░░░░░░░░░ 0 5 projects/obelisk.md
305
316
  ░░░░░░░░░░░░░░░░░░░░░░░░░ 0 5 projects/tinsel.md
306
317
  ░░░░░░░░░░░░░░░░░░░░ 0 4 archived/projects/tangerine.md
307
318
  ────
308
- 3 23 26, of 29 total
319
+ 3 27 30, of 33 total
309
320
 
310
321
  █ High ░ (Overdue/Imminent/Medium/Normal)
311
322
  EOF
@@ -326,12 +337,13 @@ function setup {
326
337
  M ~
327
338
  █████░░░░░░░░░░ 1 2 tasks.md
328
339
  ░░░░░ 0 1 projects/curtain.md
340
+ ░░░░░░░░░░░░░░░░░░░░ 0 4 projects/fountain.md
329
341
  █████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 1 7 projects/harvest.md
330
342
  ░░░░░░░░░░░░░░░░░░░░░░░░░ 0 5 projects/obelisk.md
331
343
  ░░░░░░░░░░░░░░░░░░░░░░░░░ 0 5 projects/tinsel.md
332
344
  ░░░░░░░░░░░░░░░░░░░░ 0 4 archived/projects/tangerine.md
333
345
  ────
334
- 2 24 26, of 29 total
346
+ 2 28 30, of 33 total
335
347
 
336
348
  █ Medium ░ (Overdue/Imminent/High/Normal)
337
349
  EOF
@@ -353,12 +365,13 @@ function setup {
353
365
  H N ~
354
366
  ▓▓▓▓▓▓▓▓▓░░░░░ 0 2 1 tasks.md
355
367
  ▓▓▓▓▓ 0 1 0 projects/curtain.md
368
+ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 0 4 0 projects/fountain.md
356
369
  ▚▚▚▚▚▚▚▚▚▚▚▚▚▚▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░ 3 2 3 projects/harvest.md
357
370
  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░ 0 3 2 projects/obelisk.md
358
371
  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 0 5 0 projects/tinsel.md
359
372
  ░░░░░░░░░░░░░░░░░░ 0 0 4 archived/projects/tangerine.md
360
373
  ───────
361
- 3 13 10 26, of 29 total
374
+ 3 17 10 30, of 33 total
362
375
 
363
376
  ▚ High ▓ Normal ░ (Overdue/Imminent/Medium)
364
377
  EOF
@@ -876,3 +876,70 @@ class TestNotnextParsing:
876
876
  today=date(2025, 1, 1),
877
877
  )
878
878
  assert file.notnext is False
879
+
880
+
881
+ class TestQueueParsing:
882
+ def test_file_without_queue(self):
883
+ file = MarkdownFile(
884
+ source_string="- [ ] normal task",
885
+ today=date(2025, 1, 1),
886
+ )
887
+ assert file.queue is False
888
+
889
+ def test_file_with_queue(self):
890
+ file = MarkdownFile(
891
+ source_string=dedent("""\
892
+ @queue
893
+
894
+ - [ ] first task
895
+ - [ ] second task
896
+ """),
897
+ today=date(2025, 1, 1),
898
+ )
899
+ assert file.queue is True
900
+
901
+ def test_queue_anywhere_in_file(self):
902
+ file = MarkdownFile(
903
+ source_string=dedent("""\
904
+ # Tasks
905
+
906
+ - [ ] first task
907
+
908
+ @queue
909
+ """),
910
+ today=date(2025, 1, 1),
911
+ )
912
+ assert file.queue is True
913
+
914
+ def test_escaped_queue_in_backticks(self):
915
+ file = MarkdownFile(
916
+ source_string=dedent("""\
917
+ Use `@queue` to limit output.
918
+
919
+ - [ ] first task
920
+ """),
921
+ today=date(2025, 1, 1),
922
+ )
923
+ assert file.queue is False
924
+
925
+ def test_escaped_queue_with_backslash(self):
926
+ file = MarkdownFile(
927
+ source_string=dedent("""\
928
+ Use \\@queue to limit output.
929
+
930
+ - [ ] first task
931
+ """),
932
+ today=date(2025, 1, 1),
933
+ )
934
+ assert file.queue is False
935
+
936
+ def test_queue_must_be_on_own_line(self):
937
+ file = MarkdownFile(
938
+ source_string=dedent("""\
939
+ # Tasks
940
+
941
+ - [ ] something @queue something else
942
+ """),
943
+ today=date(2025, 1, 1),
944
+ )
945
+ assert file.queue is False
@@ -46,6 +46,15 @@ subset. Prioritisation does not stop the task being ignored.
46
46
  To write about @after or deadlines without them being parsed, use backticks
47
47
  (`@after`) or a backslash (\@after).
48
48
 
49
+ For sequential work where each task in the file can only be done after the
50
+ previous tasks are complete, add @queue and output is limited to the next
51
+ available most important task.
52
+
53
+ @queue
54
+ - [ ] backup production database
55
+ - [ ] run migration to add audit columns
56
+ - [ ] backfill audit data from logs
57
+
49
58
  Context around tasks, such as detailed decision notes and designs can be
50
59
  included in the document without showing in the task list. To include a
51
60
  short note in the summary, use special fence blocks:
@@ -102,6 +102,7 @@ class Task:
102
102
  self.deferred = deferred
103
103
 
104
104
  def as_dict(self):
105
+ # provided for easier testing
105
106
  return {
106
107
  "heading": self.heading,
107
108
  "state": self.state,
@@ -172,17 +173,27 @@ class Task:
172
173
  )
173
174
 
174
175
  def wrapped_heading(self, width=80, indent=" "):
175
- if not self.heading:
176
- return []
177
- heading = self.heading
178
176
  if self.priority == Priority.OVERDUE:
179
- heading = f"{self.heading} / OVERDUE {self.format_overdue_duration()}"
177
+ suffix = f"OVERDUE {self.format_overdue_duration()}"
180
178
  elif self.priority == Priority.IMMINENT:
181
- heading = f"{self.heading} / IMMINENT {self.format_imminent_countdown()}"
179
+ suffix = f"IMMINENT {self.format_imminent_countdown()}"
182
180
  elif self.priority is None:
183
- heading = f"{self.heading} / FINISHED"
181
+ suffix = "FINISHED"
184
182
  elif self.priority != Priority.NORMAL:
185
- heading = f"{self.heading} / {self.priority.label.upper()}"
183
+ suffix = self.priority.label.upper()
184
+ else:
185
+ suffix = None
186
+
187
+ if self.heading:
188
+ if suffix:
189
+ heading = f"{self.heading} / {suffix}"
190
+ else:
191
+ heading = self.heading
192
+ elif suffix:
193
+ heading = f"# {suffix}"
194
+ else:
195
+ return []
196
+
186
197
  if width is None or len(indent + heading) <= width:
187
198
  return [indent + heading]
188
199
  return textwrap.wrap(
@@ -232,6 +243,7 @@ class MarkdownFile:
232
243
  """, re.VERBOSE)
233
244
  FILE_AFTER_PATTERN = re.compile(r"^@after(?:\s+(.+))?\s*$")
234
245
  NOTNEXT_PATTERN = re.compile(r"^@notnext(?:\s|$)")
246
+ QUEUE_PATTERN = re.compile(r"^@queue(?:\s|$)")
235
247
  DEFAULT_URGENCY = timedelta(weeks=2)
236
248
 
237
249
  def __init__(
@@ -259,6 +271,7 @@ class MarkdownFile:
259
271
  self.today = today
260
272
  self.warnings = []
261
273
  self.notnext = False
274
+ self.queue = False
262
275
  self.tasks = self.extract_tasks()
263
276
 
264
277
  @staticmethod
@@ -378,6 +391,8 @@ class MarkdownFile:
378
391
  lines.append((line_index, line))
379
392
  elif self.NOTNEXT_PATTERN.match(line):
380
393
  lines.append((line_index, line))
394
+ elif self.QUEUE_PATTERN.match(line):
395
+ lines.append((line_index, line))
381
396
 
382
397
  return lines
383
398
 
@@ -395,6 +410,8 @@ class MarkdownFile:
395
410
  file_deferred = []
396
411
  if self.NOTNEXT_PATTERN.match(line):
397
412
  self.notnext = True
413
+ if self.QUEUE_PATTERN.match(line):
414
+ self.queue = True
398
415
 
399
416
  heading = None
400
417
  priority = Priority.NORMAL
@@ -135,6 +135,44 @@ def filter_deferred(data):
135
135
  return result
136
136
 
137
137
 
138
+ def filter_queue(data):
139
+ result = []
140
+ for file, tasks in data:
141
+ if not file.queue:
142
+ result.append((file, tasks))
143
+ continue
144
+
145
+ in_progress = [
146
+ t for t in tasks
147
+ if t.state == State.IN_PROGRESS
148
+ ]
149
+ if in_progress:
150
+ result.append((file, in_progress))
151
+ continue
152
+
153
+ open_tasks = [
154
+ t for t in tasks
155
+ if t.state == State.OPEN
156
+ ]
157
+ if open_tasks:
158
+ best = min(open_tasks, key=lambda t: t.priority.value)
159
+ result.append((file, [best]))
160
+ continue
161
+
162
+ blocked = [
163
+ t for t in tasks
164
+ if t.state == State.BLOCKED
165
+ ]
166
+ if blocked:
167
+ best = min(blocked, key=lambda t: t.priority.value)
168
+ result.append((file, [best]))
169
+ continue
170
+
171
+ result.append((file, []))
172
+
173
+ return result
174
+
175
+
138
176
  def get_terminal_width():
139
177
  columns_env = os.environ.get("COLUMNS")
140
178
  if columns_env:
@@ -253,11 +291,14 @@ def flatten_by_priority(filtered_data):
253
291
  return result
254
292
 
255
293
 
294
+ _NOT_YET_SHOWN = object()
295
+
296
+
256
297
  def format_tasks(tasks, width, use_colour=False):
257
298
  output = []
258
299
  current_priority = None
259
300
  current_file = None
260
- current_heading = None
301
+ current_heading = _NOT_YET_SHOWN
261
302
 
262
303
  for task in tasks:
263
304
  if task.priority != current_priority:
@@ -265,7 +306,7 @@ def format_tasks(tasks, width, use_colour=False):
265
306
  output.append("")
266
307
  current_priority = task.priority
267
308
  current_file = None
268
- current_heading = None
309
+ current_heading = _NOT_YET_SHOWN
269
310
 
270
311
  text_colour = None
271
312
  block_colour = None
@@ -281,9 +322,9 @@ def format_tasks(tasks, width, use_colour=False):
281
322
  if task.file != current_file:
282
323
  lines.append(f"{task.file.display_path}:")
283
324
  current_file = task.file
284
- current_heading = None
325
+ current_heading = _NOT_YET_SHOWN
285
326
 
286
- if task.heading and task.heading != current_heading:
327
+ if task.heading != current_heading:
287
328
  lines.extend(task.wrapped_heading(width))
288
329
  current_heading = task.heading
289
330
  if task.annotation:
@@ -550,6 +591,9 @@ def main():
550
591
  if not args.all and not args.ignore_after:
551
592
  filtered_data = filter_deferred(filtered_data)
552
593
 
594
+ if not args.all and not args.summary:
595
+ filtered_data = filter_queue(filtered_data)
596
+
553
597
  if args.summary:
554
598
  output = format_summary(
555
599
  filtered_data,
@@ -1 +0,0 @@
1
- - [ ] I am not a task, I am a free list!
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
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