whatnext 0.0.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.
- whatnext-0.0.1/LICENCE +21 -0
- whatnext-0.0.1/LICENSE +21 -0
- whatnext-0.0.1/PKG-INFO +62 -0
- whatnext-0.0.1/README.md +38 -0
- whatnext-0.0.1/pyproject.toml +44 -0
- whatnext-0.0.1/setup.cfg +4 -0
- whatnext-0.0.1/tests/test_task.py +44 -0
- whatnext-0.0.1/whatnext/__init__.py +0 -0
- whatnext-0.0.1/whatnext/cli.py +193 -0
- whatnext-0.0.1/whatnext/models.py +150 -0
- whatnext-0.0.1/whatnext.egg-info/PKG-INFO +62 -0
- whatnext-0.0.1/whatnext.egg-info/SOURCES.txt +14 -0
- whatnext-0.0.1/whatnext.egg-info/dependency_links.txt +1 -0
- whatnext-0.0.1/whatnext.egg-info/entry_points.txt +2 -0
- whatnext-0.0.1/whatnext.egg-info/requires.txt +5 -0
- whatnext-0.0.1/whatnext.egg-info/top_level.txt +1 -0
whatnext-0.0.1/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mark Norman Francis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
whatnext-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mark Norman Francis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
whatnext-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: whatnext
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A lister of tasks found in Markdown files
|
|
5
|
+
Author: Mark Norman Francis
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/norm/whatnext
|
|
8
|
+
Project-URL: Repository, https://github.com/norm/whatnext
|
|
9
|
+
Project-URL: Documentation, https://github.com/norm/whatnext
|
|
10
|
+
Keywords: cli,markdown,tasks,todo
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENCE
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: toml
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: flake8; extra == "test"
|
|
22
|
+
Requires-Dist: pytest; extra == "test"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# What next?
|
|
26
|
+
|
|
27
|
+
Document your tasks in Markdown files, using ["standard"][sn] notation:
|
|
28
|
+
|
|
29
|
+
```markdown
|
|
30
|
+
- [ ] empty, this task is outstanding
|
|
31
|
+
- [X] crossed, this task has been completed
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then install `whatnext`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install whatnext
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Now run it and it'll tell you what's next:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
(computer)% whatnext
|
|
44
|
+
README.md:
|
|
45
|
+
# What next?
|
|
46
|
+
- [ ] empty, this task is outstanding
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
More detail to be found:
|
|
50
|
+
|
|
51
|
+
- [The basics of task formatting](docs/basics.md)
|
|
52
|
+
- [whatnext usage and arguments](docs/usage.md)
|
|
53
|
+
- [The `.whatnext` file](docs/dotwhatnext.md)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## The reason
|
|
57
|
+
|
|
58
|
+
I like to keep tasks in Markdown files. That way they can be interspersed
|
|
59
|
+
within instructions, serving as reminders, FIXMEs, and other todos.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
[sn]: https://blog.github.com/2013-01-09-task-lists-in-gfm-issues-pulls-comments/
|
whatnext-0.0.1/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# What next?
|
|
2
|
+
|
|
3
|
+
Document your tasks in Markdown files, using ["standard"][sn] notation:
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
- [ ] empty, this task is outstanding
|
|
7
|
+
- [X] crossed, this task has been completed
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Then install `whatnext`:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install whatnext
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Now run it and it'll tell you what's next:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
(computer)% whatnext
|
|
20
|
+
README.md:
|
|
21
|
+
# What next?
|
|
22
|
+
- [ ] empty, this task is outstanding
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
More detail to be found:
|
|
26
|
+
|
|
27
|
+
- [The basics of task formatting](docs/basics.md)
|
|
28
|
+
- [whatnext usage and arguments](docs/usage.md)
|
|
29
|
+
- [The `.whatnext` file](docs/dotwhatnext.md)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## The reason
|
|
33
|
+
|
|
34
|
+
I like to keep tasks in Markdown files. That way they can be interspersed
|
|
35
|
+
within instructions, serving as reminders, FIXMEs, and other todos.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
[sn]: https://blog.github.com/2013-01-09-task-lists-in-gfm-issues-pulls-comments/
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "whatnext"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "A lister of tasks found in Markdown files"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "Mark Norman Francis"}]
|
|
7
|
+
license = "MIT"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
keywords = [
|
|
10
|
+
"cli",
|
|
11
|
+
"markdown",
|
|
12
|
+
"tasks",
|
|
13
|
+
"todo",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"toml",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/norm/whatnext"
|
|
27
|
+
Repository = "https://github.com/norm/whatnext"
|
|
28
|
+
Documentation = "https://github.com/norm/whatnext"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
test = [
|
|
32
|
+
"flake8",
|
|
33
|
+
"pytest",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
whatnext = "whatnext.cli:main"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools]
|
|
40
|
+
packages = ["whatnext"]
|
|
41
|
+
|
|
42
|
+
[build-system]
|
|
43
|
+
requires = ["setuptools>=61.0"]
|
|
44
|
+
build-backend = "setuptools.build_meta"
|
whatnext-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from whatnext.models import Task, State
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TestTask:
|
|
5
|
+
task = Task(
|
|
6
|
+
"# Indicating the state of a task / Multiline tasks and indentation",
|
|
7
|
+
|
|
8
|
+
"- [ ] Lorem ipsum dolor sit amet, consectetur adipisicing elit, "
|
|
9
|
+
"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
|
10
|
+
|
|
11
|
+
State.INCOMPLETE,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def test_wraps_at_40_chars(self):
|
|
15
|
+
assert self.task.wrapped_text(width=40) == [
|
|
16
|
+
" - [ ] Lorem ipsum dolor sit amet,",
|
|
17
|
+
" consectetur adipisicing elit,",
|
|
18
|
+
" sed do eiusmod tempor",
|
|
19
|
+
" incididunt ut labore et dolore",
|
|
20
|
+
" magna aliqua.",
|
|
21
|
+
]
|
|
22
|
+
assert self.task.wrapped_heading(width=40) == [
|
|
23
|
+
" # Indicating the state of a task /",
|
|
24
|
+
" Multiline tasks and indentation",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
def test_no_wrap_at_120_chars(self):
|
|
28
|
+
assert self.task.wrapped_text(width=120) == [
|
|
29
|
+
" - [ ] Lorem ipsum dolor sit amet, consectetur adipisicing elit, "
|
|
30
|
+
"sed do eiusmod tempor incididunt ut labore et dolore",
|
|
31
|
+
" magna aliqua.",
|
|
32
|
+
]
|
|
33
|
+
assert self.task.wrapped_heading(width=120) == [
|
|
34
|
+
" # Indicating the state of a task / Multiline tasks and indentation"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
def test_default_width_80_chars(self):
|
|
38
|
+
assert self.task.wrapped_text() == [
|
|
39
|
+
" - [ ] Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do", # noqa: E501
|
|
40
|
+
" eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
|
41
|
+
]
|
|
42
|
+
assert self.task.wrapped_heading() == [
|
|
43
|
+
" # Indicating the state of a task / Multiline tasks and indentation"
|
|
44
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import fnmatch
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import toml
|
|
6
|
+
|
|
7
|
+
from whatnext.models import MarkdownFile
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_terminal_width():
|
|
11
|
+
columns_env = os.environ.get("COLUMNS")
|
|
12
|
+
if columns_env:
|
|
13
|
+
width = int(columns_env)
|
|
14
|
+
else:
|
|
15
|
+
width = shutil.get_terminal_size().columns
|
|
16
|
+
if width < 40:
|
|
17
|
+
width = 80
|
|
18
|
+
return width
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_config(config_path=None, directory="."):
|
|
22
|
+
if config_path is None:
|
|
23
|
+
config_path = os.path.join(directory, ".whatnext")
|
|
24
|
+
if os.path.exists(config_path):
|
|
25
|
+
with open(config_path) as handle:
|
|
26
|
+
return toml.load(handle)
|
|
27
|
+
return {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_ignored(filepath, ignore_patterns):
|
|
31
|
+
for pattern in ignore_patterns:
|
|
32
|
+
if fnmatch.fnmatch(filepath, pattern):
|
|
33
|
+
return True
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def find_markdown_files(directory=".", ignore_patterns=None, include_all=False):
|
|
38
|
+
if ignore_patterns is None:
|
|
39
|
+
ignore_patterns = []
|
|
40
|
+
markdown_files = []
|
|
41
|
+
for root, dirs, files in os.walk(directory):
|
|
42
|
+
for filename in files:
|
|
43
|
+
if filename.endswith(".md"):
|
|
44
|
+
filepath = os.path.join(root, filename)
|
|
45
|
+
relative_path = os.path.relpath(filepath, directory)
|
|
46
|
+
if is_ignored(relative_path, ignore_patterns):
|
|
47
|
+
continue
|
|
48
|
+
md_file = MarkdownFile(filepath, directory)
|
|
49
|
+
tasks = md_file.tasks if include_all else md_file.incomplete
|
|
50
|
+
if tasks:
|
|
51
|
+
markdown_files.append(md_file)
|
|
52
|
+
|
|
53
|
+
# files are examined depth-last as a lightweight prioritisation
|
|
54
|
+
return sorted(
|
|
55
|
+
markdown_files,
|
|
56
|
+
key=lambda file: (
|
|
57
|
+
file.display_path.count(os.sep),
|
|
58
|
+
file.display_path,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def progress_bar(complete, total, width):
|
|
64
|
+
filled = round(width * complete / total) if total else 0
|
|
65
|
+
return "█" * filled + "░" * (width - filled)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def format_summary(markdown_files, width):
|
|
69
|
+
widest = max(len(mf.tasks) for mf in markdown_files)
|
|
70
|
+
count_width = len(f"{widest}/{widest}")
|
|
71
|
+
gap = " "
|
|
72
|
+
bar_width = (
|
|
73
|
+
width
|
|
74
|
+
- count_width
|
|
75
|
+
- max(len(mf.display_path) for mf in markdown_files)
|
|
76
|
+
- len(gap) * 3
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
lines = []
|
|
80
|
+
for md_file in markdown_files:
|
|
81
|
+
total = len(md_file.tasks)
|
|
82
|
+
complete = len(md_file.complete)
|
|
83
|
+
bar = progress_bar(
|
|
84
|
+
complete,
|
|
85
|
+
total,
|
|
86
|
+
round(bar_width * total / widest)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
lines.append(
|
|
90
|
+
f"{bar.ljust(bar_width)}{gap}"
|
|
91
|
+
f"{f'{complete}/{total}'.rjust(count_width)}{gap}"
|
|
92
|
+
f"{md_file.display_path}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return "\n".join(lines)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def format_tasks(markdown_files, width, include_all):
|
|
99
|
+
lines = []
|
|
100
|
+
for md_file in markdown_files:
|
|
101
|
+
tasks = md_file.tasks if include_all else md_file.incomplete
|
|
102
|
+
lines.append(f"{md_file.display_path}:")
|
|
103
|
+
current_heading = None
|
|
104
|
+
for task in tasks:
|
|
105
|
+
if task.heading and task.heading != current_heading:
|
|
106
|
+
lines.extend(task.wrapped_heading(width))
|
|
107
|
+
current_heading = task.heading
|
|
108
|
+
lines.extend(task.wrapped_text(width))
|
|
109
|
+
return "\n".join(lines)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class CapitalisedHelpFormatter(argparse.RawDescriptionHelpFormatter):
|
|
113
|
+
def add_usage(self, usage, actions, groups, prefix=None):
|
|
114
|
+
if prefix is None:
|
|
115
|
+
prefix = "Usage: "
|
|
116
|
+
super().add_usage(usage, actions, groups, prefix)
|
|
117
|
+
|
|
118
|
+
def start_section(self, heading):
|
|
119
|
+
super().start_section(heading.capitalize() if heading else heading)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ShortHelpAction(argparse.Action):
|
|
123
|
+
def __init__(self, option_strings, dest, **kwargs):
|
|
124
|
+
super().__init__(option_strings, dest, nargs=0, **kwargs)
|
|
125
|
+
|
|
126
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
127
|
+
print(parser.format_usage().rstrip())
|
|
128
|
+
parser.exit()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main():
|
|
132
|
+
parser = argparse.ArgumentParser(
|
|
133
|
+
description="List tasks found in Markdown files",
|
|
134
|
+
epilog="""\
|
|
135
|
+
Task States:
|
|
136
|
+
- [ ] Not started (shown by default)
|
|
137
|
+
- [X] Done (hidden by default, use --all)
|
|
138
|
+
""",
|
|
139
|
+
add_help=False,
|
|
140
|
+
formatter_class=CapitalisedHelpFormatter,
|
|
141
|
+
)
|
|
142
|
+
parser.add_argument(
|
|
143
|
+
"-h",
|
|
144
|
+
action=ShortHelpAction,
|
|
145
|
+
help="Show the usage reminder and exit",
|
|
146
|
+
)
|
|
147
|
+
parser.add_argument(
|
|
148
|
+
"--help",
|
|
149
|
+
action="help",
|
|
150
|
+
help="Show this help message and exit",
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
"--dir",
|
|
154
|
+
default=".",
|
|
155
|
+
help="Directory to search (default: current directory)",
|
|
156
|
+
)
|
|
157
|
+
parser.add_argument(
|
|
158
|
+
"-s", "--summary",
|
|
159
|
+
action="store_true",
|
|
160
|
+
help="Show summary of task counts per file",
|
|
161
|
+
)
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"-a", "--all",
|
|
164
|
+
action="store_true",
|
|
165
|
+
help="Include all tasks and files, not just incomplete",
|
|
166
|
+
)
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--config",
|
|
169
|
+
help="Path to config file (default: .whatnext in search directory)",
|
|
170
|
+
)
|
|
171
|
+
parser.add_argument(
|
|
172
|
+
"--ignore",
|
|
173
|
+
action="append",
|
|
174
|
+
default=[],
|
|
175
|
+
metavar="PATTERN",
|
|
176
|
+
help="Ignore files matching pattern (can be specified multiple times)",
|
|
177
|
+
)
|
|
178
|
+
args = parser.parse_args()
|
|
179
|
+
|
|
180
|
+
config_path = args.config or os.environ.get("WHATNEXT_CONFIG")
|
|
181
|
+
config = load_config(config_path, args.dir)
|
|
182
|
+
ignore_patterns = config.get("ignore", []) + args.ignore
|
|
183
|
+
markdown_files = find_markdown_files(args.dir, ignore_patterns, args.all)
|
|
184
|
+
|
|
185
|
+
if not markdown_files:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
width = get_terminal_width()
|
|
189
|
+
|
|
190
|
+
if args.summary:
|
|
191
|
+
print(format_summary(markdown_files, width))
|
|
192
|
+
else:
|
|
193
|
+
print(format_tasks(markdown_files, width, args.all))
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import textwrap
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class State(Enum):
|
|
8
|
+
def __new__(cls, value, markers):
|
|
9
|
+
obj = object.__new__(cls)
|
|
10
|
+
obj._value_ = value
|
|
11
|
+
obj.markers = markers
|
|
12
|
+
return obj
|
|
13
|
+
|
|
14
|
+
INCOMPLETE = ("incomplete", [" "])
|
|
15
|
+
COMPLETE = ("complete", ["X", "x"])
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def all_markers(cls):
|
|
19
|
+
markers = []
|
|
20
|
+
for state in cls:
|
|
21
|
+
markers.extend(state.markers)
|
|
22
|
+
return markers
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_marker(cls, marker):
|
|
26
|
+
for state in cls:
|
|
27
|
+
if marker in state.markers:
|
|
28
|
+
return state
|
|
29
|
+
raise ValueError(f"Unknown marker: {marker}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Task:
|
|
33
|
+
def __init__(self, heading, text, state):
|
|
34
|
+
self.heading = heading
|
|
35
|
+
self.text = text
|
|
36
|
+
self.state = state
|
|
37
|
+
|
|
38
|
+
def wrapped_text(self, width=80, indent=" "):
|
|
39
|
+
text = " ".join(self.text.split())
|
|
40
|
+
if width is None or len(indent + text) <= width:
|
|
41
|
+
return [indent + text]
|
|
42
|
+
return textwrap.wrap(
|
|
43
|
+
text,
|
|
44
|
+
width=width,
|
|
45
|
+
initial_indent=indent,
|
|
46
|
+
subsequent_indent=indent + " ",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def wrapped_heading(self, width=80, indent=" "):
|
|
50
|
+
if not self.heading:
|
|
51
|
+
return []
|
|
52
|
+
if width is None or len(indent + self.heading) <= width:
|
|
53
|
+
return [indent + self.heading]
|
|
54
|
+
return textwrap.wrap(
|
|
55
|
+
self.heading,
|
|
56
|
+
width=width,
|
|
57
|
+
initial_indent=indent,
|
|
58
|
+
subsequent_indent=indent + " ",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class MarkdownFile:
|
|
63
|
+
HEADING_PATTERN = re.compile(r"""
|
|
64
|
+
^
|
|
65
|
+
(\#+) \s+ (.*)
|
|
66
|
+
$
|
|
67
|
+
""", re.VERBOSE)
|
|
68
|
+
|
|
69
|
+
VALID_STATES = re.escape("".join(State.all_markers()))
|
|
70
|
+
TASK_PATTERN = re.compile(rf"""
|
|
71
|
+
^
|
|
72
|
+
(\s*)
|
|
73
|
+
-[ ]
|
|
74
|
+
\[ ([{VALID_STATES}]) \]
|
|
75
|
+
""", re.VERBOSE)
|
|
76
|
+
|
|
77
|
+
def __init__(self, path, base_dir="."):
|
|
78
|
+
self.path = path
|
|
79
|
+
self.base_dir = base_dir
|
|
80
|
+
self.tasks = self.extract_tasks()
|
|
81
|
+
|
|
82
|
+
def extract_tasks(self):
|
|
83
|
+
tasks = []
|
|
84
|
+
for heading, lines in self.sections():
|
|
85
|
+
tasks.extend(self.tasks_in_section(heading, lines))
|
|
86
|
+
return tasks
|
|
87
|
+
|
|
88
|
+
def sections(self):
|
|
89
|
+
heading = None
|
|
90
|
+
lines = []
|
|
91
|
+
results = []
|
|
92
|
+
|
|
93
|
+
# stack stores (level, text) -- explicit level needed for
|
|
94
|
+
# skipped headings (# -> ### -> ##, where position != depth)
|
|
95
|
+
stack = []
|
|
96
|
+
|
|
97
|
+
with open(self.path) as handle:
|
|
98
|
+
for line in handle:
|
|
99
|
+
line = line.rstrip("\n")
|
|
100
|
+
if match := self.HEADING_PATTERN.match(line):
|
|
101
|
+
if lines:
|
|
102
|
+
results.append((heading, lines))
|
|
103
|
+
lines = []
|
|
104
|
+
level = len(match.group(1))
|
|
105
|
+
while stack and stack[-1][0] >= level:
|
|
106
|
+
stack.pop()
|
|
107
|
+
stack.append((level, match.group(2)))
|
|
108
|
+
heading = "# " + " / ".join(text for _, text in stack)
|
|
109
|
+
else:
|
|
110
|
+
lines.append(line)
|
|
111
|
+
|
|
112
|
+
if lines:
|
|
113
|
+
results.append((heading, lines))
|
|
114
|
+
|
|
115
|
+
return results
|
|
116
|
+
|
|
117
|
+
def tasks_in_section(self, heading, lines):
|
|
118
|
+
tasks = []
|
|
119
|
+
index = -1
|
|
120
|
+
while (index := index + 1) < len(lines):
|
|
121
|
+
if match := self.TASK_PATTERN.match(lines[index]):
|
|
122
|
+
text = lines[index].lstrip()
|
|
123
|
+
indent = len(match.group(1)) + 6
|
|
124
|
+
while (
|
|
125
|
+
index + 1 < len(lines)
|
|
126
|
+
and self.is_continuation(lines[index + 1], indent)
|
|
127
|
+
):
|
|
128
|
+
index += 1
|
|
129
|
+
text += "\n" + lines[index].strip()
|
|
130
|
+
tasks.append(Task(heading, text, State.from_marker(match.group(2))))
|
|
131
|
+
|
|
132
|
+
return tasks
|
|
133
|
+
|
|
134
|
+
def is_continuation(self, line, indent):
|
|
135
|
+
if not line.strip():
|
|
136
|
+
return False
|
|
137
|
+
leading = len(line) - len(line.lstrip())
|
|
138
|
+
return leading == indent
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def display_path(self):
|
|
142
|
+
return os.path.relpath(self.path, self.base_dir)
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def complete(self):
|
|
146
|
+
return [task for task in self.tasks if task.state == State.COMPLETE]
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def incomplete(self):
|
|
150
|
+
return [task for task in self.tasks if task.state == State.INCOMPLETE]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: whatnext
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A lister of tasks found in Markdown files
|
|
5
|
+
Author: Mark Norman Francis
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/norm/whatnext
|
|
8
|
+
Project-URL: Repository, https://github.com/norm/whatnext
|
|
9
|
+
Project-URL: Documentation, https://github.com/norm/whatnext
|
|
10
|
+
Keywords: cli,markdown,tasks,todo
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENCE
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: toml
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: flake8; extra == "test"
|
|
22
|
+
Requires-Dist: pytest; extra == "test"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# What next?
|
|
26
|
+
|
|
27
|
+
Document your tasks in Markdown files, using ["standard"][sn] notation:
|
|
28
|
+
|
|
29
|
+
```markdown
|
|
30
|
+
- [ ] empty, this task is outstanding
|
|
31
|
+
- [X] crossed, this task has been completed
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then install `whatnext`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install whatnext
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Now run it and it'll tell you what's next:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
(computer)% whatnext
|
|
44
|
+
README.md:
|
|
45
|
+
# What next?
|
|
46
|
+
- [ ] empty, this task is outstanding
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
More detail to be found:
|
|
50
|
+
|
|
51
|
+
- [The basics of task formatting](docs/basics.md)
|
|
52
|
+
- [whatnext usage and arguments](docs/usage.md)
|
|
53
|
+
- [The `.whatnext` file](docs/dotwhatnext.md)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## The reason
|
|
57
|
+
|
|
58
|
+
I like to keep tasks in Markdown files. That way they can be interspersed
|
|
59
|
+
within instructions, serving as reminders, FIXMEs, and other todos.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
[sn]: https://blog.github.com/2013-01-09-task-lists-in-gfm-issues-pulls-comments/
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENCE
|
|
2
|
+
LICENSE
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
tests/test_task.py
|
|
6
|
+
whatnext/__init__.py
|
|
7
|
+
whatnext/cli.py
|
|
8
|
+
whatnext/models.py
|
|
9
|
+
whatnext.egg-info/PKG-INFO
|
|
10
|
+
whatnext.egg-info/SOURCES.txt
|
|
11
|
+
whatnext.egg-info/dependency_links.txt
|
|
12
|
+
whatnext.egg-info/entry_points.txt
|
|
13
|
+
whatnext.egg-info/requires.txt
|
|
14
|
+
whatnext.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
whatnext
|