whatnext 3.3.2__tar.gz → 3.4__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.
- {whatnext-3.3.2 → whatnext-3.4}/CHANGELOG.md +10 -1
- {whatnext-3.3.2 → whatnext-3.4}/PKG-INFO +1 -1
- {whatnext-3.3.2 → whatnext-3.4}/docs/usage.md +5 -0
- whatnext-3.4/tasks/muting.md +18 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/config.bats +10 -0
- whatnext-3.4/tests/mute/.whatnext.mute +4 -0
- whatnext-3.4/tests/mute/.whatnext.mute.expected +4 -0
- whatnext-3.4/tests/mute/tasks.md +7 -0
- whatnext-3.4/tests/mute-fixture/tasks.md +7 -0
- whatnext-3.4/tests/mute.bats +180 -0
- whatnext-3.4/tests/test_config.py +43 -0
- whatnext-3.4/tests/test_mute.py +115 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/usage.bats +11 -6
- {whatnext-3.3.2 → whatnext-3.4}/whatnext/next.py +4 -2
- {whatnext-3.3.2 → whatnext-3.4}/whatnext/whatnext.py +142 -8
- {whatnext-3.3.2 → whatnext-3.4}/.flake8 +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/.github/dependabot.yml +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/.github/workflows/publish.yml +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/.github/workflows/tests.yml +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/.gitignore +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/.whatnext +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/LICENCE +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/LICENSE +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/Makefile +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/README.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/archive/done/tasks.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/docs/annotations.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/docs/basics.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/docs/deadlines.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/docs/deferring.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/docs/exclusions.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/docs/index.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/docs/installation.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/docs/next.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/docs/prioritisation.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/docs/sequential.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/example/archived/projects/tangerine.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/example/projects/curtain.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/example/projects/fountain.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/example/projects/harvest.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/example/projects/obelisk.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/example/projects/tinsel.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/example/tasks.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/example/template.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/pyproject.toml +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/scripts/update-usage-examples.py +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/deferring.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/documentation.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/ignoring.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/listing.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/next.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/summaries.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/test_refactor.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/version_0.1.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/version_0.2.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/version_0.3.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/version_0.4.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/version_0.5.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/version_1.0.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/version_1.1.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/version_1.2.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks/version_1.3.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tasks.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/colour.bats +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/config/.whatnext +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/config/.whatnext.alt +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/config/apples.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/config/oranges.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/config-external +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deadline.bats +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/bare-after.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/circular-a.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/circular-b.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/deferred.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/dir-complete/tasks.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/duplicate-warnings.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/missing-dep.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/needs-absolute.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/needs-dir-complete.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/needs-subdir-file.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/needs-subdir.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/phase-misplaced.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/phases-empty.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/phases-partial.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/phases-queue.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/phases.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/regular.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/root-target.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/samename.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/subdir/needs-parent.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/subdir/needs-sibling.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/subdir/samename.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring/subdir/target.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/deferring.bats +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/edit.bats +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/headerless/annotation.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/headerless/deadline.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/housekeeping.bats +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/invalid.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/list.bats +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/matching.bats +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/next/alternate.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/next/append-after.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/next/append.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/next/empty.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/next/insert.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/next/projects/alpha/tasks/things.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/next/projects/alpha/tasks.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/next/projects/beta/things.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/next/something.txt +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/next/tasks.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/next/wide.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/next/wrapped.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/next.bats +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/notnext.bats +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/queue/all-complete.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/queue/blocked.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/queue/in-progress.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/queue/normal.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/queue/prioritised.md +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/queue.bats +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/summary.bats +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/test_defer.py +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/test_edit.py +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/test_file.py +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/test_limit.py +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/test_output.py +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/test_summary.py +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/test_task.py +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/tests/version.bats +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/whatnext/__init__.py +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/whatnext/guide.txt +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/whatnext/models.py +0 -0
- {whatnext-3.3.2 → whatnext-3.4}/whatnext/summary.py +0 -0
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## [v3.4] - 2026-04-17
|
|
5
|
+
|
|
6
|
+
**Added:**
|
|
7
|
+
- Task muting: temporarily hide tasks matching patterns from output by
|
|
8
|
+
using `whatnext --mute [time] [match]`.
|
|
9
|
+
|
|
10
|
+
|
|
4
11
|
## [v3.3.2] - 2026-04-17
|
|
5
12
|
|
|
6
13
|
**Fixes:** `@after` with a directory waits on all tasks under that dir.
|
|
@@ -62,12 +69,14 @@ Previously `whatnext` assumed a flat hierarchy of unique filenames.
|
|
|
62
69
|
the file; the first phase is always visible, and subsequent phases
|
|
63
70
|
will only show when all previous phases are complete.
|
|
64
71
|
|
|
72
|
+
## [v2.1.1] - 2026-02-10
|
|
73
|
+
|
|
65
74
|
**Fixes:**
|
|
66
75
|
- `@after` referencing a file excluded by `--ignore` no longer warns about
|
|
67
76
|
the file not existing, instead treating the dependency as complete.
|
|
68
77
|
|
|
69
78
|
|
|
70
|
-
## [v2.
|
|
79
|
+
## [v2.1] - 2026-02-09
|
|
71
80
|
|
|
72
81
|
Added `@queue` directive show only one task at a time, for simple sequential
|
|
73
82
|
work that doesn't need to be split across files using `@after`.
|
|
@@ -232,6 +232,11 @@ To show a random selection of tasks rather than by priority, use `5r`.
|
|
|
232
232
|
|
|
233
233
|
- `--ignore-all` — ignore all deferral constraints (`@after`, `@phase`, `@queue`).
|
|
234
234
|
|
|
235
|
+
- `--ignore-mute` — ignore mute settings.
|
|
236
|
+
|
|
237
|
+
- `--mute [time] [match]` — for the next 'time' period (formatted `4d`, `2w`, `1m2d`)
|
|
238
|
+
mute any tasks with 'match' in the text, header, or filename.
|
|
239
|
+
|
|
235
240
|
- `-o` / `--open` — show only open tasks.
|
|
236
241
|
|
|
237
242
|
- `-p` / `--partial` — show only in progress tasks.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
There is a difference between the _truth_ of the tasks as captured in the
|
|
2
|
+
files, where dependencies are declared, and the _availability_ of what can or
|
|
3
|
+
will be done by an individual on a team, so the person running `whatnext`
|
|
4
|
+
should be able to temporarily or permanently ignore a task or group of tasks
|
|
5
|
+
from showing up as available for them to work on.
|
|
6
|
+
|
|
7
|
+
- [X] `.whatnext.mute` merges with `.whatnext` to populate the 'mute' key in
|
|
8
|
+
the config
|
|
9
|
+
- contains a list, each entry being a timestamp and a match pattern
|
|
10
|
+
- if the timestamp is now in the past, the entry is removed
|
|
11
|
+
- [X] listing tasks uses the mute list
|
|
12
|
+
- anything matching a pattern is excluded from output
|
|
13
|
+
- `--ignore-mute`, `--ignore-all`, `--all` bypasses this
|
|
14
|
+
- show a count of muted tasks at the end
|
|
15
|
+
- [X] `whatnext --mute 'period' 'pattern'` creates the entry
|
|
16
|
+
- period is formatted like 1d, 2d, 1w, 1m, 2m3w
|
|
17
|
+
- pattern cannot be empty
|
|
18
|
+
- [ ] if `whatnext [...]` produces no tasks, try again without the mute list
|
|
@@ -82,3 +82,13 @@ function setup {
|
|
|
82
82
|
diff -u <(echo "$expected_output") <(echo "$output")
|
|
83
83
|
[ $status -eq 0 ]
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
@test "expired mute entries removed from file" {
|
|
87
|
+
cp -r "$BATS_TEST_DIRNAME/mute/." "$BATS_TEST_TMPDIR"
|
|
88
|
+
|
|
89
|
+
run --separate-stderr \
|
|
90
|
+
whatnext --dir "$BATS_TEST_TMPDIR"
|
|
91
|
+
|
|
92
|
+
diff -u "$BATS_TEST_DIRNAME/mute/.whatnext.mute.expected" "$BATS_TEST_TMPDIR/.whatnext.mute"
|
|
93
|
+
[ $status -eq 0 ]
|
|
94
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
bats_require_minimum_version 1.5.0
|
|
2
|
+
|
|
3
|
+
@test "--mute creates mute entry" {
|
|
4
|
+
run whatnext --config "$BATS_TEST_TMPDIR/.whatnext" --mute 1d "apples"
|
|
5
|
+
|
|
6
|
+
# check pattern is in file (until timestamp varies)
|
|
7
|
+
diff -u <(echo 'pattern = "apples"') <(grep 'pattern' "$BATS_TEST_TMPDIR/.whatnext.mute")
|
|
8
|
+
[ $status -eq 0 ]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@test "--mute appends to existing file" {
|
|
12
|
+
sed -e 's/^ //' > "$BATS_TEST_TMPDIR/.whatnext.mute" <<-EOF
|
|
13
|
+
[[mute]]
|
|
14
|
+
until = 2099-12-31T23:59:59
|
|
15
|
+
pattern = "existing"
|
|
16
|
+
EOF
|
|
17
|
+
|
|
18
|
+
run whatnext --config "$BATS_TEST_TMPDIR/.whatnext" --mute 1w "new"
|
|
19
|
+
|
|
20
|
+
expected=$(sed -e 's/^ //' <<-EOF
|
|
21
|
+
pattern = "existing"
|
|
22
|
+
pattern = "new"
|
|
23
|
+
EOF
|
|
24
|
+
)
|
|
25
|
+
diff -u <(echo "$expected") <(grep 'pattern' "$BATS_TEST_TMPDIR/.whatnext.mute")
|
|
26
|
+
[ $status -eq 0 ]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@test "--mute rejects empty pattern" {
|
|
30
|
+
run --separate-stderr \
|
|
31
|
+
whatnext --config "$BATS_TEST_TMPDIR/.whatnext" --mute 1d ""
|
|
32
|
+
|
|
33
|
+
diff -u <(echo "ERROR: pattern cannot be empty") <(echo "$stderr")
|
|
34
|
+
[ $status -eq 1 ]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@test "--mute rejects invalid period" {
|
|
38
|
+
run --separate-stderr \
|
|
39
|
+
whatnext --config "$BATS_TEST_TMPDIR/.whatnext" --mute "abc" "pattern"
|
|
40
|
+
|
|
41
|
+
diff -u <(echo "ERROR: invalid period 'abc'") <(echo "$stderr")
|
|
42
|
+
[ $status -eq 1 ]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@test "muted tasks are excluded from output" {
|
|
46
|
+
sed -e 's/^ //' > "$BATS_TEST_TMPDIR/.whatnext.mute" <<-EOF
|
|
47
|
+
[[mute]]
|
|
48
|
+
until = 2099-12-31T23:59:59
|
|
49
|
+
pattern = "apples"
|
|
50
|
+
EOF
|
|
51
|
+
|
|
52
|
+
expected_output=$(sed -e 's/^ //' <<-EOF
|
|
53
|
+
tasks.md:
|
|
54
|
+
# Shopping
|
|
55
|
+
- [ ] buy oranges
|
|
56
|
+
# Gardening
|
|
57
|
+
- [ ] plant seeds
|
|
58
|
+
- [ ] water plants
|
|
59
|
+
|
|
60
|
+
(1 task muted)
|
|
61
|
+
EOF
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
run --separate-stderr \
|
|
65
|
+
whatnext --config "$BATS_TEST_TMPDIR/.whatnext" tests/mute
|
|
66
|
+
|
|
67
|
+
diff -u <(echo "$expected_output") <(echo "$output")
|
|
68
|
+
[[ $status -eq 0 ]]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@test "muting matches heading" {
|
|
72
|
+
sed -e 's/^ //' > "$BATS_TEST_TMPDIR/.whatnext.mute" <<-EOF
|
|
73
|
+
[[mute]]
|
|
74
|
+
until = 2099-12-31T23:59:59
|
|
75
|
+
pattern = "shopping"
|
|
76
|
+
EOF
|
|
77
|
+
|
|
78
|
+
expected_output=$(sed -e 's/^ //' <<-EOF
|
|
79
|
+
tasks.md:
|
|
80
|
+
# Gardening
|
|
81
|
+
- [ ] plant seeds
|
|
82
|
+
- [ ] water plants
|
|
83
|
+
|
|
84
|
+
(2 tasks muted)
|
|
85
|
+
EOF
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
run --separate-stderr \
|
|
89
|
+
whatnext --config "$BATS_TEST_TMPDIR/.whatnext" tests/mute
|
|
90
|
+
|
|
91
|
+
diff -u <(echo "$expected_output") <(echo "$output")
|
|
92
|
+
[[ $status -eq 0 ]]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@test "muting matches filename" {
|
|
96
|
+
sed -e 's/^ //' > "$BATS_TEST_TMPDIR/.whatnext.mute" <<-EOF
|
|
97
|
+
[[mute]]
|
|
98
|
+
until = 2099-12-31T23:59:59
|
|
99
|
+
pattern = "tasks"
|
|
100
|
+
EOF
|
|
101
|
+
|
|
102
|
+
expected_output=$(sed -e 's/^ //' <<-EOF
|
|
103
|
+
Only muted tasks remain:
|
|
104
|
+
|
|
105
|
+
tasks.md:
|
|
106
|
+
# Shopping
|
|
107
|
+
- [ ] buy apples
|
|
108
|
+
- [ ] buy oranges
|
|
109
|
+
# Gardening
|
|
110
|
+
- [ ] plant seeds
|
|
111
|
+
- [ ] water plants
|
|
112
|
+
EOF
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
run --separate-stderr \
|
|
116
|
+
whatnext --config "$BATS_TEST_TMPDIR/.whatnext" tests/mute
|
|
117
|
+
|
|
118
|
+
diff -u <(echo "$expected_output") <(echo "$output")
|
|
119
|
+
[[ $status -eq 0 ]]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@test "multiple patterns exclude multiple tasks" {
|
|
123
|
+
sed -e 's/^ //' > "$BATS_TEST_TMPDIR/.whatnext.mute" <<-EOF
|
|
124
|
+
[[mute]]
|
|
125
|
+
until = 2099-12-31T23:59:59
|
|
126
|
+
pattern = "apples"
|
|
127
|
+
|
|
128
|
+
[[mute]]
|
|
129
|
+
until = 2099-12-31T23:59:59
|
|
130
|
+
pattern = "seeds"
|
|
131
|
+
EOF
|
|
132
|
+
|
|
133
|
+
expected_output=$(sed -e 's/^ //' <<-EOF
|
|
134
|
+
tasks.md:
|
|
135
|
+
# Shopping
|
|
136
|
+
- [ ] buy oranges
|
|
137
|
+
# Gardening
|
|
138
|
+
- [ ] water plants
|
|
139
|
+
|
|
140
|
+
(2 tasks muted)
|
|
141
|
+
EOF
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
run --separate-stderr \
|
|
145
|
+
whatnext --config "$BATS_TEST_TMPDIR/.whatnext" tests/mute
|
|
146
|
+
|
|
147
|
+
diff -u <(echo "$expected_output") <(echo "$output")
|
|
148
|
+
[[ $status -eq 0 ]]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@test "options bypass mute filtering" {
|
|
152
|
+
sed -e 's/^ //' > "$BATS_TEST_TMPDIR/.whatnext.mute" <<-EOF
|
|
153
|
+
[[mute]]
|
|
154
|
+
until = 2099-12-31T23:59:59
|
|
155
|
+
pattern = "apples"
|
|
156
|
+
EOF
|
|
157
|
+
|
|
158
|
+
# no count when nothing muted
|
|
159
|
+
expected_output=$(sed -e 's/^ //' <<-EOF
|
|
160
|
+
tasks.md:
|
|
161
|
+
# Shopping
|
|
162
|
+
- [ ] buy apples
|
|
163
|
+
- [ ] buy oranges
|
|
164
|
+
# Gardening
|
|
165
|
+
- [ ] plant seeds
|
|
166
|
+
- [ ] water plants
|
|
167
|
+
EOF
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
for flag in --ignore-mute --all --ignore-all; do
|
|
171
|
+
run --separate-stderr \
|
|
172
|
+
whatnext \
|
|
173
|
+
--config "$BATS_TEST_TMPDIR/.whatnext" \
|
|
174
|
+
"$flag" \
|
|
175
|
+
tests/mute
|
|
176
|
+
|
|
177
|
+
diff -u <(echo "$expected_output") <(echo "$output")
|
|
178
|
+
[[ $status -eq 0 ]]
|
|
179
|
+
done
|
|
180
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from whatnext.whatnext import load_config, parse_period, resolve_config_path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestParsePeriod:
|
|
10
|
+
def test_days(self):
|
|
11
|
+
assert parse_period("1d") == timedelta(days=1)
|
|
12
|
+
assert parse_period("5d") == timedelta(days=5)
|
|
13
|
+
|
|
14
|
+
def test_weeks(self):
|
|
15
|
+
assert parse_period("1w") == timedelta(weeks=1)
|
|
16
|
+
assert parse_period("2w") == timedelta(weeks=2)
|
|
17
|
+
|
|
18
|
+
def test_months(self):
|
|
19
|
+
assert parse_period("1m") == timedelta(days=30)
|
|
20
|
+
assert parse_period("2m") == timedelta(days=60)
|
|
21
|
+
|
|
22
|
+
def test_combined(self):
|
|
23
|
+
assert parse_period("2m3w") == timedelta(days=60 + 21)
|
|
24
|
+
assert parse_period("1m2w3d") == timedelta(days=30 + 14 + 3)
|
|
25
|
+
|
|
26
|
+
def test_invalid_format_raises(self):
|
|
27
|
+
with pytest.raises(ValueError):
|
|
28
|
+
parse_period("")
|
|
29
|
+
with pytest.raises(ValueError):
|
|
30
|
+
parse_period("abc")
|
|
31
|
+
with pytest.raises(ValueError):
|
|
32
|
+
parse_period("1x")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestMuteLoading:
|
|
36
|
+
def test_expired_mutes_filtered_from_config(self, tmp_path):
|
|
37
|
+
shutil.copytree("tests/mute", tmp_path / "mute")
|
|
38
|
+
config_path = resolve_config_path(None, str(tmp_path / "mute"))
|
|
39
|
+
config = load_config(config_path, extensions=["mute"])
|
|
40
|
+
|
|
41
|
+
patterns = [entry["pattern"] for entry in config["mute"]]
|
|
42
|
+
assert "future" in patterns
|
|
43
|
+
assert "old" not in patterns
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from textwrap import dedent
|
|
3
|
+
|
|
4
|
+
from whatnext.models import MarkdownFile
|
|
5
|
+
from whatnext.whatnext import filter_muted
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def make_data(source_string):
|
|
9
|
+
file = MarkdownFile(source_string=source_string, today=date(2025, 1, 1))
|
|
10
|
+
return [(file, file.tasks)]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestFilterMuted:
|
|
14
|
+
def test_empty_mute_list_returns_all_tasks(self):
|
|
15
|
+
data = make_data(dedent("""\
|
|
16
|
+
- [ ] first task
|
|
17
|
+
- [ ] second task
|
|
18
|
+
"""))
|
|
19
|
+
|
|
20
|
+
filtered, count = filter_muted(data, [])
|
|
21
|
+
|
|
22
|
+
assert len(filtered[0][1]) == 2
|
|
23
|
+
assert count == 0
|
|
24
|
+
|
|
25
|
+
def test_pattern_matches_task_text(self):
|
|
26
|
+
data = make_data(dedent("""\
|
|
27
|
+
- [ ] buy apples
|
|
28
|
+
- [ ] buy oranges
|
|
29
|
+
"""))
|
|
30
|
+
|
|
31
|
+
filtered, count = filter_muted(data, ["apples"])
|
|
32
|
+
|
|
33
|
+
assert len(filtered[0][1]) == 1
|
|
34
|
+
assert filtered[0][1][0].text == "buy oranges"
|
|
35
|
+
assert count == 1
|
|
36
|
+
|
|
37
|
+
def test_pattern_matches_heading(self):
|
|
38
|
+
data = make_data(dedent("""\
|
|
39
|
+
# Shopping
|
|
40
|
+
- [ ] buy apples
|
|
41
|
+
# Gardening
|
|
42
|
+
- [ ] plant seeds
|
|
43
|
+
"""))
|
|
44
|
+
|
|
45
|
+
filtered, count = filter_muted(data, ["shopping"])
|
|
46
|
+
|
|
47
|
+
assert len(filtered[0][1]) == 1
|
|
48
|
+
assert filtered[0][1][0].text == "plant seeds"
|
|
49
|
+
assert count == 1
|
|
50
|
+
|
|
51
|
+
def test_matching_is_case_insensitive(self):
|
|
52
|
+
data = make_data(dedent("""\
|
|
53
|
+
- [ ] Buy APPLES
|
|
54
|
+
- [ ] buy oranges
|
|
55
|
+
"""))
|
|
56
|
+
|
|
57
|
+
filtered, count = filter_muted(data, ["apples"])
|
|
58
|
+
|
|
59
|
+
assert len(filtered[0][1]) == 1
|
|
60
|
+
assert filtered[0][1][0].text == "buy oranges"
|
|
61
|
+
assert count == 1
|
|
62
|
+
|
|
63
|
+
def test_multiple_patterns_any_match_excludes(self):
|
|
64
|
+
data = make_data(dedent("""\
|
|
65
|
+
- [ ] buy apples
|
|
66
|
+
- [ ] buy oranges
|
|
67
|
+
- [ ] buy bananas
|
|
68
|
+
"""))
|
|
69
|
+
|
|
70
|
+
filtered, count = filter_muted(data, ["apples", "oranges"])
|
|
71
|
+
|
|
72
|
+
assert len(filtered[0][1]) == 1
|
|
73
|
+
assert filtered[0][1][0].text == "buy bananas"
|
|
74
|
+
assert count == 2
|
|
75
|
+
|
|
76
|
+
def test_no_match_includes_task(self):
|
|
77
|
+
data = make_data(dedent("""\
|
|
78
|
+
- [ ] buy apples
|
|
79
|
+
- [ ] buy oranges
|
|
80
|
+
"""))
|
|
81
|
+
|
|
82
|
+
filtered, count = filter_muted(data, ["bananas"])
|
|
83
|
+
|
|
84
|
+
assert len(filtered[0][1]) == 2
|
|
85
|
+
assert count == 0
|
|
86
|
+
|
|
87
|
+
def test_counts_across_multiple_files(self):
|
|
88
|
+
file1 = MarkdownFile(
|
|
89
|
+
source_string="- [ ] buy apples\n- [ ] buy oranges\n",
|
|
90
|
+
today=date(2025, 1, 1),
|
|
91
|
+
)
|
|
92
|
+
file2 = MarkdownFile(
|
|
93
|
+
source_string="- [ ] plant apples\n- [ ] plant oranges\n",
|
|
94
|
+
today=date(2025, 1, 1),
|
|
95
|
+
)
|
|
96
|
+
data = [(file1, file1.tasks), (file2, file2.tasks)]
|
|
97
|
+
|
|
98
|
+
filtered, count = filter_muted(data, ["apples"])
|
|
99
|
+
|
|
100
|
+
assert len(filtered[0][1]) == 1
|
|
101
|
+
assert len(filtered[1][1]) == 1
|
|
102
|
+
assert count == 2
|
|
103
|
+
|
|
104
|
+
def test_pattern_matches_filename(self):
|
|
105
|
+
file = MarkdownFile(
|
|
106
|
+
source_string="- [ ] first task\n- [ ] second task\n",
|
|
107
|
+
path="shopping.md",
|
|
108
|
+
today=date(2025, 1, 1),
|
|
109
|
+
)
|
|
110
|
+
data = [(file, file.tasks)]
|
|
111
|
+
|
|
112
|
+
filtered, count = filter_muted(data, ["shopping"])
|
|
113
|
+
|
|
114
|
+
assert len(filtered[0][1]) == 0
|
|
115
|
+
assert count == 2
|
|
@@ -8,9 +8,10 @@ bats_require_minimum_version 1.5.0
|
|
|
8
8
|
expected_output=$(sed -e 's/^ //' <<" EOF"
|
|
9
9
|
Usage: whatnext [-h] [--help] [--version] [--guide] [--dir DIR] [-s] [-e]
|
|
10
10
|
[--relative] [-a] [--ignore-after] [--ignore-queue]
|
|
11
|
-
[--ignore-phase] [--ignore-all] [--
|
|
12
|
-
[--
|
|
13
|
-
[
|
|
11
|
+
[--ignore-phase] [--ignore-all] [--ignore-mute]
|
|
12
|
+
[--mute TIME MATCH] [--config CONFIG] [--ignore PATTERN] [-q]
|
|
13
|
+
[-o] [-p] [-b] [-d] [-c] [--priority LEVEL]
|
|
14
|
+
[--color | --no-color]
|
|
14
15
|
[match ...]
|
|
15
16
|
EOF
|
|
16
17
|
)
|
|
@@ -26,9 +27,10 @@ bats_require_minimum_version 1.5.0
|
|
|
26
27
|
expected_output=$(sed -e 's/^ //' <<" EOF"
|
|
27
28
|
Usage: whatnext [-h] [--help] [--version] [--guide] [--dir DIR] [-s] [-e]
|
|
28
29
|
[--relative] [-a] [--ignore-after] [--ignore-queue]
|
|
29
|
-
[--ignore-phase] [--ignore-all] [--
|
|
30
|
-
[--
|
|
31
|
-
[
|
|
30
|
+
[--ignore-phase] [--ignore-all] [--ignore-mute]
|
|
31
|
+
[--mute TIME MATCH] [--config CONFIG] [--ignore PATTERN] [-q]
|
|
32
|
+
[-o] [-p] [-b] [-d] [-c] [--priority LEVEL]
|
|
33
|
+
[--color | --no-color]
|
|
32
34
|
[match ...]
|
|
33
35
|
|
|
34
36
|
List tasks found in Markdown files
|
|
@@ -58,6 +60,9 @@ bats_require_minimum_version 1.5.0
|
|
|
58
60
|
--ignore-phase Ignore @phase constraints
|
|
59
61
|
--ignore-all Ignore all deferral constraints (@after, @phase,
|
|
60
62
|
@queue)
|
|
63
|
+
--ignore-mute Ignore mute settings
|
|
64
|
+
--mute TIME MATCH Mute tasks matching pattern for period (e.g. 1d, 2w,
|
|
65
|
+
1m)
|
|
61
66
|
--config CONFIG Path to config file (default: WHATNEXT_CONFIG, or
|
|
62
67
|
'.whatnext' in --dir)
|
|
63
68
|
--ignore PATTERN Ignore files matching pattern (can be specified
|
|
@@ -7,7 +7,7 @@ import sys
|
|
|
7
7
|
import textwrap
|
|
8
8
|
|
|
9
9
|
from whatnext.models import MarkdownFile
|
|
10
|
-
from whatnext.whatnext import load_config
|
|
10
|
+
from whatnext.whatnext import load_config, resolve_config_path
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def rewind_insertion_point(lines, position, min_position):
|
|
@@ -63,7 +63,9 @@ def get_wrap_width(lines, tasks_file, config_path):
|
|
|
63
63
|
if env_width:
|
|
64
64
|
default_width = int(env_width)
|
|
65
65
|
else:
|
|
66
|
-
|
|
66
|
+
directory = os.path.dirname(tasks_file) or "."
|
|
67
|
+
resolved_path = resolve_config_path(config_path, directory)
|
|
68
|
+
config = load_config(resolved_path)
|
|
67
69
|
default_width = config.get("wrap_width", 80)
|
|
68
70
|
file_width = detect_task_width(lines)
|
|
69
71
|
return max(file_width, default_width)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import argparse
|
|
2
|
-
from datetime import date
|
|
2
|
+
from datetime import date, datetime, timedelta
|
|
3
3
|
import fnmatch
|
|
4
4
|
import importlib.metadata
|
|
5
5
|
import importlib.resources
|
|
@@ -29,6 +29,28 @@ PRIORITY_COLOURS = {
|
|
|
29
29
|
ACTIVE_STATES = {State.OPEN, State.IN_PROGRESS, State.BLOCKED}
|
|
30
30
|
COMPLETE_STATES = {State.COMPLETE, State.CANCELLED}
|
|
31
31
|
|
|
32
|
+
PERIOD_PATTERN = re.compile(r'(\d+)([dwm])')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_period(period_str):
|
|
36
|
+
matches = PERIOD_PATTERN.findall(period_str)
|
|
37
|
+
if not matches:
|
|
38
|
+
raise ValueError(f"invalid period '{period_str}'")
|
|
39
|
+
# verify the entire string was consumed
|
|
40
|
+
reconstructed = ''.join(f"{n}{u}" for n, u in matches)
|
|
41
|
+
if reconstructed != period_str:
|
|
42
|
+
raise ValueError(f"invalid period '{period_str}'")
|
|
43
|
+
total_days = 0
|
|
44
|
+
for amount, unit in matches:
|
|
45
|
+
amount = int(amount)
|
|
46
|
+
if unit == 'd':
|
|
47
|
+
total_days += amount
|
|
48
|
+
elif unit == 'w':
|
|
49
|
+
total_days += amount * 7
|
|
50
|
+
elif unit == 'm':
|
|
51
|
+
total_days += amount * 30
|
|
52
|
+
return timedelta(days=total_days)
|
|
53
|
+
|
|
32
54
|
|
|
33
55
|
class CircularDependencyError(Exception):
|
|
34
56
|
pass
|
|
@@ -226,6 +248,35 @@ def filter_phases(data):
|
|
|
226
248
|
return result
|
|
227
249
|
|
|
228
250
|
|
|
251
|
+
def filter_muted(data, mute_patterns):
|
|
252
|
+
if not mute_patterns:
|
|
253
|
+
return data, 0
|
|
254
|
+
|
|
255
|
+
muted_count = 0
|
|
256
|
+
result = []
|
|
257
|
+
for file, tasks in data:
|
|
258
|
+
file_lower = file.display_path.lower()
|
|
259
|
+
filtered_tasks = []
|
|
260
|
+
for task in tasks:
|
|
261
|
+
heading_lower = ""
|
|
262
|
+
if task.heading:
|
|
263
|
+
heading_lower = task.heading.lower()
|
|
264
|
+
text_lower = task.text.lower()
|
|
265
|
+
is_muted = any(
|
|
266
|
+
pattern in file_lower
|
|
267
|
+
or pattern in heading_lower
|
|
268
|
+
or pattern in text_lower
|
|
269
|
+
for pattern in mute_patterns
|
|
270
|
+
)
|
|
271
|
+
if is_muted:
|
|
272
|
+
muted_count += 1
|
|
273
|
+
else:
|
|
274
|
+
filtered_tasks.append(task)
|
|
275
|
+
result.append((file, filtered_tasks))
|
|
276
|
+
|
|
277
|
+
return result, muted_count
|
|
278
|
+
|
|
279
|
+
|
|
229
280
|
def get_terminal_width():
|
|
230
281
|
columns_env = os.environ.get("COLUMNS")
|
|
231
282
|
if columns_env:
|
|
@@ -244,15 +295,45 @@ def get_editor():
|
|
|
244
295
|
return "vi"
|
|
245
296
|
|
|
246
297
|
|
|
247
|
-
def
|
|
298
|
+
def resolve_config_path(config_path, directory):
|
|
248
299
|
if config_path is None:
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
300
|
+
return os.path.join(directory, ".whatnext")
|
|
301
|
+
if config_path.startswith("./") or os.path.isabs(config_path):
|
|
302
|
+
return config_path
|
|
303
|
+
return os.path.join(directory, config_path)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def write_mute_config(config_path, mutes):
|
|
307
|
+
mute_path = config_path + ".mute"
|
|
308
|
+
with open(mute_path, "w") as handle:
|
|
309
|
+
toml.dump({"mute": mutes}, handle)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def load_config(config_path, extensions=None):
|
|
252
313
|
if os.path.exists(config_path):
|
|
253
314
|
with open(config_path) as handle:
|
|
254
|
-
|
|
255
|
-
|
|
315
|
+
config = toml.load(handle)
|
|
316
|
+
else:
|
|
317
|
+
config = {}
|
|
318
|
+
|
|
319
|
+
if extensions:
|
|
320
|
+
for ext in extensions:
|
|
321
|
+
ext_path = config_path + "." + ext
|
|
322
|
+
if os.path.exists(ext_path):
|
|
323
|
+
with open(ext_path) as handle:
|
|
324
|
+
config[ext] = toml.load(handle).get(ext, [])
|
|
325
|
+
|
|
326
|
+
if "mute" in config:
|
|
327
|
+
now = datetime.now()
|
|
328
|
+
original = config["mute"]
|
|
329
|
+
config["mute"] = [
|
|
330
|
+
entry for entry in original
|
|
331
|
+
if entry["until"] > now
|
|
332
|
+
]
|
|
333
|
+
if len(config["mute"]) != len(original):
|
|
334
|
+
write_mute_config(config_path, config["mute"])
|
|
335
|
+
|
|
336
|
+
return config
|
|
256
337
|
|
|
257
338
|
|
|
258
339
|
def is_ignored(filepath, ignore_patterns):
|
|
@@ -522,6 +603,17 @@ def main():
|
|
|
522
603
|
action="store_true",
|
|
523
604
|
help="Ignore all deferral constraints (@after, @phase, @queue)",
|
|
524
605
|
)
|
|
606
|
+
parser.add_argument(
|
|
607
|
+
"--ignore-mute",
|
|
608
|
+
action="store_true",
|
|
609
|
+
help="Ignore mute settings",
|
|
610
|
+
)
|
|
611
|
+
parser.add_argument(
|
|
612
|
+
"--mute",
|
|
613
|
+
nargs=2,
|
|
614
|
+
metavar=("TIME", "MATCH"),
|
|
615
|
+
help="Mute tasks matching pattern for period (e.g. 1d, 2w, 1m)",
|
|
616
|
+
)
|
|
525
617
|
parser.add_argument(
|
|
526
618
|
"--config",
|
|
527
619
|
default=os.environ.get("WHATNEXT_CONFIG"),
|
|
@@ -593,6 +685,24 @@ def main():
|
|
|
593
685
|
" [n]r - limit to n results, selected at random",
|
|
594
686
|
)
|
|
595
687
|
args = parser.parse_args()
|
|
688
|
+
config_path = resolve_config_path(args.config, args.dir)
|
|
689
|
+
|
|
690
|
+
if args.mute:
|
|
691
|
+
period_str, pattern = args.mute
|
|
692
|
+
if not pattern:
|
|
693
|
+
print("ERROR: pattern cannot be empty", file=sys.stderr)
|
|
694
|
+
sys.exit(1)
|
|
695
|
+
try:
|
|
696
|
+
period = parse_period(period_str)
|
|
697
|
+
except ValueError as e:
|
|
698
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
699
|
+
sys.exit(1)
|
|
700
|
+
until = datetime.now() + period
|
|
701
|
+
config = load_config(config_path, extensions=["mute"])
|
|
702
|
+
mutes = config.get("mute", [])
|
|
703
|
+
mutes.append({"until": until, "pattern": pattern})
|
|
704
|
+
write_mute_config(config_path, mutes)
|
|
705
|
+
return
|
|
596
706
|
|
|
597
707
|
# build the search space
|
|
598
708
|
paths = []
|
|
@@ -613,8 +723,12 @@ def main():
|
|
|
613
723
|
if not paths:
|
|
614
724
|
paths = [args.dir]
|
|
615
725
|
|
|
616
|
-
config = load_config(
|
|
726
|
+
config = load_config(config_path, extensions=["mute"])
|
|
617
727
|
ignore_patterns = config.get("ignore", []) + args.ignore
|
|
728
|
+
mute_patterns = [
|
|
729
|
+
entry["pattern"].lower()
|
|
730
|
+
for entry in config.get("mute", [])
|
|
731
|
+
]
|
|
618
732
|
quiet = args.quiet
|
|
619
733
|
|
|
620
734
|
if "WHATNEXT_TODAY" in os.environ:
|
|
@@ -687,6 +801,12 @@ def main():
|
|
|
687
801
|
):
|
|
688
802
|
filtered_data = filter_queue(filtered_data)
|
|
689
803
|
|
|
804
|
+
muted_count = 0
|
|
805
|
+
pre_mute_data = None
|
|
806
|
+
if not args.all and not args.ignore_all and not args.ignore_mute:
|
|
807
|
+
pre_mute_data = filtered_data
|
|
808
|
+
filtered_data, muted_count = filter_muted(filtered_data, mute_patterns)
|
|
809
|
+
|
|
690
810
|
if args.summary:
|
|
691
811
|
output = format_summary(
|
|
692
812
|
filtered_data,
|
|
@@ -701,6 +821,10 @@ def main():
|
|
|
701
821
|
)
|
|
702
822
|
else:
|
|
703
823
|
tasks = flatten_by_priority(filtered_data)
|
|
824
|
+
all_muted = False
|
|
825
|
+
if not tasks and muted_count > 0:
|
|
826
|
+
tasks = flatten_by_priority(pre_mute_data)
|
|
827
|
+
all_muted = True
|
|
704
828
|
if randomise:
|
|
705
829
|
random.shuffle(tasks)
|
|
706
830
|
if limit:
|
|
@@ -716,6 +840,10 @@ def main():
|
|
|
716
840
|
for line, filepath in files_to_edit:
|
|
717
841
|
subprocess.run([editor, f"+{line}", filepath])
|
|
718
842
|
|
|
843
|
+
if all_muted:
|
|
844
|
+
print("Only muted tasks remain:\n")
|
|
845
|
+
muted_count = 0
|
|
846
|
+
|
|
719
847
|
output = format_tasks(
|
|
720
848
|
tasks,
|
|
721
849
|
get_terminal_width(),
|
|
@@ -723,3 +851,9 @@ def main():
|
|
|
723
851
|
)
|
|
724
852
|
|
|
725
853
|
print(output)
|
|
854
|
+
|
|
855
|
+
if muted_count > 0:
|
|
856
|
+
noun = "tasks"
|
|
857
|
+
if muted_count == 1:
|
|
858
|
+
noun = "task"
|
|
859
|
+
print(f"\n({muted_count} {noun} muted)")
|
|
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
|
|
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
|