checkmarks 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checkmarks-0.3.0/MANIFEST.in +2 -0
- {checkmarks-0.2.0/src/checkmarks.egg-info → checkmarks-0.3.0}/PKG-INFO +30 -23
- {checkmarks-0.2.0 → checkmarks-0.3.0}/README.md +29 -22
- {checkmarks-0.2.0 → checkmarks-0.3.0}/pyproject.toml +1 -1
- {checkmarks-0.2.0 → checkmarks-0.3.0}/src/checkmarks/parse.py +2 -29
- {checkmarks-0.2.0 → checkmarks-0.3.0}/src/checkmarks/render.py +0 -2
- {checkmarks-0.2.0 → checkmarks-0.3.0/src/checkmarks.egg-info}/PKG-INFO +30 -23
- {checkmarks-0.2.0 → checkmarks-0.3.0}/src/checkmarks.egg-info/SOURCES.txt +6 -1
- checkmarks-0.3.0/tests/fixtures/empty.md +3 -0
- checkmarks-0.3.0/tests/fixtures/heading_in_fence.md +6 -0
- checkmarks-0.3.0/tests/fixtures/quoted_title.md +6 -0
- checkmarks-0.3.0/tests/fixtures/unclosed_frontmatter.md +4 -0
- {checkmarks-0.2.0 → checkmarks-0.3.0}/tests/test_cli.py +7 -7
- {checkmarks-0.2.0 → checkmarks-0.3.0}/tests/test_parse.py +5 -62
- {checkmarks-0.2.0 → checkmarks-0.3.0}/tests/test_render.py +19 -22
- {checkmarks-0.2.0 → checkmarks-0.3.0}/LICENSE +0 -0
- {checkmarks-0.2.0 → checkmarks-0.3.0}/setup.cfg +0 -0
- {checkmarks-0.2.0 → checkmarks-0.3.0}/src/checkmarks/__init__.py +0 -0
- {checkmarks-0.2.0 → checkmarks-0.3.0}/src/checkmarks/cli.py +0 -0
- {checkmarks-0.2.0 → checkmarks-0.3.0}/src/checkmarks/collect.py +0 -0
- {checkmarks-0.2.0 → checkmarks-0.3.0}/src/checkmarks.egg-info/dependency_links.txt +0 -0
- {checkmarks-0.2.0 → checkmarks-0.3.0}/src/checkmarks.egg-info/entry_points.txt +0 -0
- {checkmarks-0.2.0 → checkmarks-0.3.0}/src/checkmarks.egg-info/top_level.txt +0 -0
- {checkmarks-0.2.0 → checkmarks-0.3.0}/tests/test_collect.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: checkmarks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Progress dashboard for Markdown checklists
|
|
5
5
|
Author-email: menisadi <menisadi@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -39,10 +39,10 @@ A small command-line dashboard for your progress. Point it at a folder of Markdo
|
|
|
39
39
|
|
|
40
40
|
```
|
|
41
41
|
$ checkmarks ~/progress
|
|
42
|
-
Thinking, Fast and Slow [████------]
|
|
43
|
-
Gödel, Escher, Bach [██--------]
|
|
44
|
-
Rust tutorial [████------]
|
|
45
|
-
NN hands-on [██--------]
|
|
42
|
+
Thinking, Fast and Slow [████------] 17/38 chapters 45%
|
|
43
|
+
Gödel, Escher, Bach [██--------] 5/22 chapters 23%
|
|
44
|
+
Rust tutorial [████------] 12/28 tasks 43%
|
|
45
|
+
NN hands-on [██--------] 2/8 tasks 25%
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
Your Markdown files are the source of truth: checkmarks only reads and reports. It never changes your files and keeps no state of its own.
|
|
@@ -96,27 +96,30 @@ By default, checkmarks counts checkboxes:
|
|
|
96
96
|
- List items can start with `-`, `*`, `+`, `1.` or `1)`.
|
|
97
97
|
- Checkboxes inside fenced code blocks are ignored.
|
|
98
98
|
|
|
99
|
-
###
|
|
99
|
+
### Big things: break them down
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
checkmarks only counts checkboxes. To track a book, split it into chapters or page ranges, each one a task small enough to finish in a sitting:
|
|
102
102
|
|
|
103
103
|
```markdown
|
|
104
104
|
---
|
|
105
|
-
|
|
106
|
-
unit: pages
|
|
105
|
+
unit: chunks
|
|
107
106
|
---
|
|
108
107
|
# Dune
|
|
108
|
+
|
|
109
|
+
- [x] pp. 1–40
|
|
110
|
+
- [x] pp. 41–80
|
|
111
|
+
- [ ] pp. 81–120
|
|
112
|
+
- [ ] pp. 121–160
|
|
109
113
|
```
|
|
110
114
|
|
|
111
|
-
Supported keys:
|
|
115
|
+
Supported frontmatter keys:
|
|
112
116
|
|
|
113
|
-
| Key
|
|
114
|
-
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `title` | Row title |
|
|
117
|
+
| Key | Meaning |
|
|
118
|
+
|---------|------------------------------------------|
|
|
119
|
+
| `unit` | Label after the count (default `tasks`) |
|
|
120
|
+
| `title` | Row title |
|
|
118
121
|
|
|
119
|
-
|
|
122
|
+
Other keys are ignored.
|
|
120
123
|
|
|
121
124
|
### Titles
|
|
122
125
|
|
|
@@ -129,7 +132,7 @@ On a terminal, checkmarks prints the aligned table shown above. Rows at 100% are
|
|
|
129
132
|
When output is piped, it switches to tab-separated rows with no header:
|
|
130
133
|
|
|
131
134
|
```
|
|
132
|
-
title done total pct unit
|
|
135
|
+
title done total pct unit status path
|
|
133
136
|
```
|
|
134
137
|
|
|
135
138
|
Use `--json` for a JSON array:
|
|
@@ -139,18 +142,17 @@ $ checkmarks --json dune.md
|
|
|
139
142
|
[
|
|
140
143
|
{
|
|
141
144
|
"title": "Dune",
|
|
142
|
-
"done":
|
|
143
|
-
"total":
|
|
144
|
-
"pct":
|
|
145
|
-
"unit": "
|
|
146
|
-
"mode": "counter",
|
|
145
|
+
"done": 9,
|
|
146
|
+
"total": 22,
|
|
147
|
+
"pct": 40.9,
|
|
148
|
+
"unit": "chapters",
|
|
147
149
|
"status": "ok",
|
|
148
150
|
"path": "dune.md"
|
|
149
151
|
}
|
|
150
152
|
]
|
|
151
153
|
```
|
|
152
154
|
|
|
153
|
-
`status` is `ok`, `empty` (no checkboxes) or `error` (
|
|
155
|
+
`status` is `ok`, `empty` (no checkboxes) or `error` (the file can't be read). Problems are reported on stderr, and the row is still shown.
|
|
154
156
|
|
|
155
157
|
Empty files found in folders are hidden; pass `--all` to show them. Files you name directly are always shown.
|
|
156
158
|
|
|
@@ -167,6 +169,11 @@ Empty files found in folders are hidden; pass `--all` to show them. Files you na
|
|
|
167
169
|
|
|
168
170
|
Exit code is `0` if at least one file has progress to show, `1` if none does, and `2` for usage errors.
|
|
169
171
|
|
|
172
|
+
## Upgrading from 0.2
|
|
173
|
+
|
|
174
|
+
- The `progress: N/M` frontmatter key (counter mode) is gone. Replace it with a checklist of chapters or page ranges.
|
|
175
|
+
- The `mode` column (TSV) and field (JSON) are removed.
|
|
176
|
+
|
|
170
177
|
## Upgrading from 0.1
|
|
171
178
|
|
|
172
179
|
- Output is now a table row per file instead of the old "📊 Progress for:" block.
|
|
@@ -4,10 +4,10 @@ A small command-line dashboard for your progress. Point it at a folder of Markdo
|
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
$ checkmarks ~/progress
|
|
7
|
-
Thinking, Fast and Slow [████------]
|
|
8
|
-
Gödel, Escher, Bach [██--------]
|
|
9
|
-
Rust tutorial [████------]
|
|
10
|
-
NN hands-on [██--------]
|
|
7
|
+
Thinking, Fast and Slow [████------] 17/38 chapters 45%
|
|
8
|
+
Gödel, Escher, Bach [██--------] 5/22 chapters 23%
|
|
9
|
+
Rust tutorial [████------] 12/28 tasks 43%
|
|
10
|
+
NN hands-on [██--------] 2/8 tasks 25%
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Your Markdown files are the source of truth: checkmarks only reads and reports. It never changes your files and keeps no state of its own.
|
|
@@ -61,27 +61,30 @@ By default, checkmarks counts checkboxes:
|
|
|
61
61
|
- List items can start with `-`, `*`, `+`, `1.` or `1)`.
|
|
62
62
|
- Checkboxes inside fenced code blocks are ignored.
|
|
63
63
|
|
|
64
|
-
###
|
|
64
|
+
### Big things: break them down
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
checkmarks only counts checkboxes. To track a book, split it into chapters or page ranges, each one a task small enough to finish in a sitting:
|
|
67
67
|
|
|
68
68
|
```markdown
|
|
69
69
|
---
|
|
70
|
-
|
|
71
|
-
unit: pages
|
|
70
|
+
unit: chunks
|
|
72
71
|
---
|
|
73
72
|
# Dune
|
|
73
|
+
|
|
74
|
+
- [x] pp. 1–40
|
|
75
|
+
- [x] pp. 41–80
|
|
76
|
+
- [ ] pp. 81–120
|
|
77
|
+
- [ ] pp. 121–160
|
|
74
78
|
```
|
|
75
79
|
|
|
76
|
-
Supported keys:
|
|
80
|
+
Supported frontmatter keys:
|
|
77
81
|
|
|
78
|
-
| Key
|
|
79
|
-
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `title` | Row title |
|
|
82
|
+
| Key | Meaning |
|
|
83
|
+
|---------|------------------------------------------|
|
|
84
|
+
| `unit` | Label after the count (default `tasks`) |
|
|
85
|
+
| `title` | Row title |
|
|
83
86
|
|
|
84
|
-
|
|
87
|
+
Other keys are ignored.
|
|
85
88
|
|
|
86
89
|
### Titles
|
|
87
90
|
|
|
@@ -94,7 +97,7 @@ On a terminal, checkmarks prints the aligned table shown above. Rows at 100% are
|
|
|
94
97
|
When output is piped, it switches to tab-separated rows with no header:
|
|
95
98
|
|
|
96
99
|
```
|
|
97
|
-
title done total pct unit
|
|
100
|
+
title done total pct unit status path
|
|
98
101
|
```
|
|
99
102
|
|
|
100
103
|
Use `--json` for a JSON array:
|
|
@@ -104,18 +107,17 @@ $ checkmarks --json dune.md
|
|
|
104
107
|
[
|
|
105
108
|
{
|
|
106
109
|
"title": "Dune",
|
|
107
|
-
"done":
|
|
108
|
-
"total":
|
|
109
|
-
"pct":
|
|
110
|
-
"unit": "
|
|
111
|
-
"mode": "counter",
|
|
110
|
+
"done": 9,
|
|
111
|
+
"total": 22,
|
|
112
|
+
"pct": 40.9,
|
|
113
|
+
"unit": "chapters",
|
|
112
114
|
"status": "ok",
|
|
113
115
|
"path": "dune.md"
|
|
114
116
|
}
|
|
115
117
|
]
|
|
116
118
|
```
|
|
117
119
|
|
|
118
|
-
`status` is `ok`, `empty` (no checkboxes) or `error` (
|
|
120
|
+
`status` is `ok`, `empty` (no checkboxes) or `error` (the file can't be read). Problems are reported on stderr, and the row is still shown.
|
|
119
121
|
|
|
120
122
|
Empty files found in folders are hidden; pass `--all` to show them. Files you name directly are always shown.
|
|
121
123
|
|
|
@@ -132,6 +134,11 @@ Empty files found in folders are hidden; pass `--all` to show them. Files you na
|
|
|
132
134
|
|
|
133
135
|
Exit code is `0` if at least one file has progress to show, `1` if none does, and `2` for usage errors.
|
|
134
136
|
|
|
137
|
+
## Upgrading from 0.2
|
|
138
|
+
|
|
139
|
+
- The `progress: N/M` frontmatter key (counter mode) is gone. Replace it with a checklist of chapters or page ranges.
|
|
140
|
+
- The `mode` column (TSV) and field (JSON) are removed.
|
|
141
|
+
|
|
135
142
|
## Upgrading from 0.1
|
|
136
143
|
|
|
137
144
|
- Output is now a table row per file instead of the old "📊 Progress for:" block.
|
|
@@ -7,7 +7,6 @@ from typing import Literal
|
|
|
7
7
|
CHECKBOX_RE = re.compile(r"^\s*(?:[-*+]|\d+[.)])\s+\[( |x|X)\](?:\s|$)")
|
|
8
8
|
FENCE_RE = re.compile(r"^\s*(`{3,}|~{3,})")
|
|
9
9
|
HEADING_RE = re.compile(r"^#\s+(.+)")
|
|
10
|
-
PROGRESS_RE = re.compile(r"^(\d+)\s*/\s*(\d+)$")
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
@dataclass
|
|
@@ -17,7 +16,6 @@ class Item:
|
|
|
17
16
|
done: int | None
|
|
18
17
|
total: int | None
|
|
19
18
|
unit: str
|
|
20
|
-
mode: Literal["checklist", "counter"]
|
|
21
19
|
status: Literal["ok", "empty", "error"]
|
|
22
20
|
message: str | None = None
|
|
23
21
|
|
|
@@ -89,20 +87,6 @@ def parse_frontmatter(lines: list[str]) -> tuple[dict[str, str], int]:
|
|
|
89
87
|
return meta, end + 1
|
|
90
88
|
|
|
91
89
|
|
|
92
|
-
def parse_progress(value: str) -> tuple[int, int] | str:
|
|
93
|
-
"""Returns (done, total), or an error message if the value is invalid."""
|
|
94
|
-
progress_match = PROGRESS_RE.match(value)
|
|
95
|
-
if not progress_match:
|
|
96
|
-
return f"unparseable progress: {value!r}"
|
|
97
|
-
|
|
98
|
-
done, total = int(progress_match.group(1)), int(progress_match.group(2))
|
|
99
|
-
if total == 0:
|
|
100
|
-
return "progress total is 0"
|
|
101
|
-
if done > total:
|
|
102
|
-
return f"progress {done}/{total} exceeds total"
|
|
103
|
-
return done, total
|
|
104
|
-
|
|
105
|
-
|
|
106
90
|
def parse_file(path: Path) -> Item:
|
|
107
91
|
"""Parses a Markdown file into an Item."""
|
|
108
92
|
try:
|
|
@@ -114,7 +98,6 @@ def parse_file(path: Path) -> Item:
|
|
|
114
98
|
done=None,
|
|
115
99
|
total=None,
|
|
116
100
|
unit="tasks",
|
|
117
|
-
mode="checklist",
|
|
118
101
|
status="error",
|
|
119
102
|
message=str(e),
|
|
120
103
|
)
|
|
@@ -124,18 +107,8 @@ def parse_file(path: Path) -> Item:
|
|
|
124
107
|
body = lines[body_start:]
|
|
125
108
|
title = meta.get("title") or find_heading(body) or path.stem
|
|
126
109
|
|
|
127
|
-
if "progress" in meta:
|
|
128
|
-
unit = meta.get("unit") or "units"
|
|
129
|
-
progress = parse_progress(meta["progress"])
|
|
130
|
-
if isinstance(progress, str):
|
|
131
|
-
return Item(path, title, None, None, unit, "counter", "error", progress)
|
|
132
|
-
done, total = progress
|
|
133
|
-
return Item(path, title, done, total, unit, "counter", "ok")
|
|
134
|
-
|
|
135
110
|
unit = meta.get("unit") or "tasks"
|
|
136
111
|
done, total = count_checkboxes("\n".join(body))
|
|
137
112
|
if total == 0:
|
|
138
|
-
return Item(
|
|
139
|
-
|
|
140
|
-
)
|
|
141
|
-
return Item(path, title, done, total, unit, "checklist", "ok")
|
|
113
|
+
return Item(path, title, None, None, unit, "empty", "no checkboxes found")
|
|
114
|
+
return Item(path, title, done, total, unit, "ok")
|
|
@@ -33,7 +33,6 @@ def render_tsv(items: list[Item]) -> str:
|
|
|
33
33
|
str(item.total) if ok else "",
|
|
34
34
|
f"{p:.1f}" if ok else "",
|
|
35
35
|
clean(item.unit),
|
|
36
|
-
item.mode,
|
|
37
36
|
item.status,
|
|
38
37
|
str(item.path),
|
|
39
38
|
]
|
|
@@ -53,7 +52,6 @@ def render_json(items: list[Item]) -> str:
|
|
|
53
52
|
"total": item.total if ok else None,
|
|
54
53
|
"pct": round(p, 1) if p is not None else None,
|
|
55
54
|
"unit": item.unit,
|
|
56
|
-
"mode": item.mode,
|
|
57
55
|
"status": item.status,
|
|
58
56
|
"path": str(item.path),
|
|
59
57
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: checkmarks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Progress dashboard for Markdown checklists
|
|
5
5
|
Author-email: menisadi <menisadi@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -39,10 +39,10 @@ A small command-line dashboard for your progress. Point it at a folder of Markdo
|
|
|
39
39
|
|
|
40
40
|
```
|
|
41
41
|
$ checkmarks ~/progress
|
|
42
|
-
Thinking, Fast and Slow [████------]
|
|
43
|
-
Gödel, Escher, Bach [██--------]
|
|
44
|
-
Rust tutorial [████------]
|
|
45
|
-
NN hands-on [██--------]
|
|
42
|
+
Thinking, Fast and Slow [████------] 17/38 chapters 45%
|
|
43
|
+
Gödel, Escher, Bach [██--------] 5/22 chapters 23%
|
|
44
|
+
Rust tutorial [████------] 12/28 tasks 43%
|
|
45
|
+
NN hands-on [██--------] 2/8 tasks 25%
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
Your Markdown files are the source of truth: checkmarks only reads and reports. It never changes your files and keeps no state of its own.
|
|
@@ -96,27 +96,30 @@ By default, checkmarks counts checkboxes:
|
|
|
96
96
|
- List items can start with `-`, `*`, `+`, `1.` or `1)`.
|
|
97
97
|
- Checkboxes inside fenced code blocks are ignored.
|
|
98
98
|
|
|
99
|
-
###
|
|
99
|
+
### Big things: break them down
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
checkmarks only counts checkboxes. To track a book, split it into chapters or page ranges, each one a task small enough to finish in a sitting:
|
|
102
102
|
|
|
103
103
|
```markdown
|
|
104
104
|
---
|
|
105
|
-
|
|
106
|
-
unit: pages
|
|
105
|
+
unit: chunks
|
|
107
106
|
---
|
|
108
107
|
# Dune
|
|
108
|
+
|
|
109
|
+
- [x] pp. 1–40
|
|
110
|
+
- [x] pp. 41–80
|
|
111
|
+
- [ ] pp. 81–120
|
|
112
|
+
- [ ] pp. 121–160
|
|
109
113
|
```
|
|
110
114
|
|
|
111
|
-
Supported keys:
|
|
115
|
+
Supported frontmatter keys:
|
|
112
116
|
|
|
113
|
-
| Key
|
|
114
|
-
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `title` | Row title |
|
|
117
|
+
| Key | Meaning |
|
|
118
|
+
|---------|------------------------------------------|
|
|
119
|
+
| `unit` | Label after the count (default `tasks`) |
|
|
120
|
+
| `title` | Row title |
|
|
118
121
|
|
|
119
|
-
|
|
122
|
+
Other keys are ignored.
|
|
120
123
|
|
|
121
124
|
### Titles
|
|
122
125
|
|
|
@@ -129,7 +132,7 @@ On a terminal, checkmarks prints the aligned table shown above. Rows at 100% are
|
|
|
129
132
|
When output is piped, it switches to tab-separated rows with no header:
|
|
130
133
|
|
|
131
134
|
```
|
|
132
|
-
title done total pct unit
|
|
135
|
+
title done total pct unit status path
|
|
133
136
|
```
|
|
134
137
|
|
|
135
138
|
Use `--json` for a JSON array:
|
|
@@ -139,18 +142,17 @@ $ checkmarks --json dune.md
|
|
|
139
142
|
[
|
|
140
143
|
{
|
|
141
144
|
"title": "Dune",
|
|
142
|
-
"done":
|
|
143
|
-
"total":
|
|
144
|
-
"pct":
|
|
145
|
-
"unit": "
|
|
146
|
-
"mode": "counter",
|
|
145
|
+
"done": 9,
|
|
146
|
+
"total": 22,
|
|
147
|
+
"pct": 40.9,
|
|
148
|
+
"unit": "chapters",
|
|
147
149
|
"status": "ok",
|
|
148
150
|
"path": "dune.md"
|
|
149
151
|
}
|
|
150
152
|
]
|
|
151
153
|
```
|
|
152
154
|
|
|
153
|
-
`status` is `ok`, `empty` (no checkboxes) or `error` (
|
|
155
|
+
`status` is `ok`, `empty` (no checkboxes) or `error` (the file can't be read). Problems are reported on stderr, and the row is still shown.
|
|
154
156
|
|
|
155
157
|
Empty files found in folders are hidden; pass `--all` to show them. Files you name directly are always shown.
|
|
156
158
|
|
|
@@ -167,6 +169,11 @@ Empty files found in folders are hidden; pass `--all` to show them. Files you na
|
|
|
167
169
|
|
|
168
170
|
Exit code is `0` if at least one file has progress to show, `1` if none does, and `2` for usage errors.
|
|
169
171
|
|
|
172
|
+
## Upgrading from 0.2
|
|
173
|
+
|
|
174
|
+
- The `progress: N/M` frontmatter key (counter mode) is gone. Replace it with a checklist of chapters or page ranges.
|
|
175
|
+
- The `mode` column (TSV) and field (JSON) are removed.
|
|
176
|
+
|
|
170
177
|
## Upgrading from 0.1
|
|
171
178
|
|
|
172
179
|
- Output is now a table row per file instead of the old "📊 Progress for:" block.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
2
3
|
README.md
|
|
3
4
|
pyproject.toml
|
|
4
5
|
src/checkmarks/__init__.py
|
|
@@ -14,4 +15,8 @@ src/checkmarks.egg-info/top_level.txt
|
|
|
14
15
|
tests/test_cli.py
|
|
15
16
|
tests/test_collect.py
|
|
16
17
|
tests/test_parse.py
|
|
17
|
-
tests/test_render.py
|
|
18
|
+
tests/test_render.py
|
|
19
|
+
tests/fixtures/empty.md
|
|
20
|
+
tests/fixtures/heading_in_fence.md
|
|
21
|
+
tests/fixtures/quoted_title.md
|
|
22
|
+
tests/fixtures/unclosed_frontmatter.md
|
|
@@ -12,10 +12,10 @@ def tree(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
|
12
12
|
"""A progress folder, used as the current directory."""
|
|
13
13
|
(tmp_path / "a.md").write_text("# Alpha\n- [x] a\n- [ ] b\n", encoding="utf-8")
|
|
14
14
|
(tmp_path / "b.md").write_text(
|
|
15
|
-
"---\
|
|
15
|
+
"---\nunit: chapters\n---\n# Beta\n- [x] a\n", encoding="utf-8"
|
|
16
16
|
)
|
|
17
17
|
(tmp_path / "notes.md").write_text("# Notes\nprose\n", encoding="utf-8")
|
|
18
|
-
(tmp_path / "bad.md").
|
|
18
|
+
(tmp_path / "bad.md").write_bytes(b"- [x] caf\xe9\n")
|
|
19
19
|
monkeypatch.chdir(tmp_path)
|
|
20
20
|
monkeypatch.delenv("CHECKMARKS_DIR", raising=False)
|
|
21
21
|
monkeypatch.delenv("NO_COLOR", raising=False)
|
|
@@ -31,11 +31,11 @@ def test_default_is_tsv_when_piped(tree: Path, capsys: pytest.CaptureFixture) ->
|
|
|
31
31
|
assert main([]) == 0
|
|
32
32
|
out, err = capsys.readouterr()
|
|
33
33
|
assert out.splitlines() == [
|
|
34
|
-
"Alpha\t1\t2\t50.0\ttasks\
|
|
35
|
-
"Beta\
|
|
36
|
-
"bad\t\t\t\
|
|
34
|
+
"Alpha\t1\t2\t50.0\ttasks\tok\ta.md",
|
|
35
|
+
"Beta\t1\t1\t100.0\tchapters\tok\tb.md",
|
|
36
|
+
"bad\t\t\t\ttasks\terror\tbad.md",
|
|
37
37
|
]
|
|
38
|
-
assert err
|
|
38
|
+
assert err.startswith("checkmarks: bad.md: ")
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def test_default_is_pretty_on_tty(
|
|
@@ -86,7 +86,7 @@ def test_directory_hides_empty_unless_all(
|
|
|
86
86
|
main(["."])
|
|
87
87
|
assert "Notes" not in capsys.readouterr().out
|
|
88
88
|
main([".", "--all"])
|
|
89
|
-
assert "Notes\t\t\t\ttasks\
|
|
89
|
+
assert "Notes\t\t\t\ttasks\tempty\tnotes.md" in capsys.readouterr().out
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
def test_explicit_empty_file_shown(tree: Path, capsys: pytest.CaptureFixture) -> None:
|
|
@@ -3,12 +3,10 @@ from pathlib import Path
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
5
|
from checkmarks.parse import (
|
|
6
|
-
Item,
|
|
7
6
|
count_checkboxes,
|
|
8
7
|
find_heading,
|
|
9
8
|
parse_file,
|
|
10
9
|
parse_frontmatter,
|
|
11
|
-
parse_progress,
|
|
12
10
|
)
|
|
13
11
|
|
|
14
12
|
|
|
@@ -93,8 +91,8 @@ FIXTURES = Path(__file__).parent / "fixtures"
|
|
|
93
91
|
|
|
94
92
|
|
|
95
93
|
def test_parse_frontmatter_basic() -> None:
|
|
96
|
-
meta, body_start = parse_frontmatter(["---", "
|
|
97
|
-
assert meta == {"
|
|
94
|
+
meta, body_start = parse_frontmatter(["---", "unit: pages", "---", "# T"])
|
|
95
|
+
assert meta == {"unit": "pages"}
|
|
98
96
|
assert body_start == 3
|
|
99
97
|
|
|
100
98
|
|
|
@@ -124,44 +122,11 @@ def test_parse_frontmatter_quotes(value: str, expected: str) -> None:
|
|
|
124
122
|
assert meta["title"] == expected
|
|
125
123
|
|
|
126
124
|
|
|
127
|
-
@pytest.mark.parametrize(
|
|
128
|
-
"value,expected",
|
|
129
|
-
[
|
|
130
|
-
("143/320", (143, 320)),
|
|
131
|
-
("143 / 320", (143, 320)),
|
|
132
|
-
("0/5", (0, 5)),
|
|
133
|
-
("5/5", (5, 5)),
|
|
134
|
-
],
|
|
135
|
-
)
|
|
136
|
-
def test_parse_progress_valid(value: str, expected: tuple[int, int]) -> None:
|
|
137
|
-
assert parse_progress(value) == expected
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
@pytest.mark.parametrize("value", ["", "half", "1/", "/2", "-1/5", "1.5/3", "1/2/3"])
|
|
141
|
-
def test_parse_progress_unparseable(value: str) -> None:
|
|
142
|
-
assert isinstance(parse_progress(value), str)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def test_parse_progress_zero_total() -> None:
|
|
146
|
-
assert parse_progress("0/0") == "progress total is 0"
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def test_parse_progress_done_exceeds_total() -> None:
|
|
150
|
-
assert parse_progress("6/5") == "progress 6/5 exceeds total"
|
|
151
|
-
|
|
152
|
-
|
|
153
125
|
def test_find_heading_skips_fences() -> None:
|
|
154
126
|
assert find_heading(["```", "# comment", "```", "# Real"]) == "Real"
|
|
155
127
|
assert find_heading(["## Sub", "no heading"]) is None
|
|
156
128
|
|
|
157
129
|
|
|
158
|
-
def test_file_counter_mode() -> None:
|
|
159
|
-
item = parse_file(FIXTURES / "counter.md")
|
|
160
|
-
assert item == Item(
|
|
161
|
-
FIXTURES / "counter.md", "Dune", 143, 320, "pages", "counter", "ok"
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
|
|
165
130
|
def test_file_bom_and_crlf(tmp_path: Path) -> None:
|
|
166
131
|
# Written here, not as a fixture, so git's autocrlf can't strip the CRLFs
|
|
167
132
|
md_file = tmp_path / "bom_crlf.md"
|
|
@@ -175,33 +140,18 @@ def test_file_bom_and_crlf(tmp_path: Path) -> None:
|
|
|
175
140
|
def test_file_unclosed_frontmatter_is_markdown() -> None:
|
|
176
141
|
item = parse_file(FIXTURES / "unclosed_frontmatter.md")
|
|
177
142
|
assert item.title == "unclosed_frontmatter"
|
|
178
|
-
assert (item.done, item.total
|
|
143
|
+
assert (item.done, item.total) == (1, 2)
|
|
179
144
|
|
|
180
145
|
|
|
181
146
|
def test_file_frontmatter_title_wins_over_heading() -> None:
|
|
182
147
|
item = parse_file(FIXTURES / "quoted_title.md")
|
|
183
148
|
assert item.title == "Dune: Part One"
|
|
184
149
|
assert item.unit == "chapters"
|
|
185
|
-
assert item.mode == "checklist"
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def test_file_bad_progress_is_error() -> None:
|
|
189
|
-
item = parse_file(FIXTURES / "bad_progress.md")
|
|
190
|
-
assert (item.mode, item.status) == ("counter", "error")
|
|
191
|
-
assert (item.done, item.total) == (None, None)
|
|
192
|
-
assert item.title == "Bad progress"
|
|
193
|
-
assert item.unit == "units"
|
|
194
|
-
assert item.message is not None
|
|
195
150
|
|
|
196
151
|
|
|
197
152
|
def test_file_without_checkboxes_is_empty() -> None:
|
|
198
153
|
item = parse_file(FIXTURES / "empty.md")
|
|
199
|
-
assert (item.
|
|
200
|
-
"checklist",
|
|
201
|
-
"empty",
|
|
202
|
-
None,
|
|
203
|
-
None,
|
|
204
|
-
)
|
|
154
|
+
assert (item.status, item.done, item.total) == ("empty", None, None)
|
|
205
155
|
assert item.unit == "tasks"
|
|
206
156
|
|
|
207
157
|
|
|
@@ -213,7 +163,7 @@ def test_file_heading_inside_fence_is_not_title() -> None:
|
|
|
213
163
|
|
|
214
164
|
def test_file_missing_is_error(tmp_path: Path) -> None:
|
|
215
165
|
item = parse_file(tmp_path / "missing.md")
|
|
216
|
-
assert (item.
|
|
166
|
+
assert (item.status, item.title) == ("error", "missing")
|
|
217
167
|
|
|
218
168
|
|
|
219
169
|
def test_file_invalid_utf8_is_error(tmp_path: Path) -> None:
|
|
@@ -221,10 +171,3 @@ def test_file_invalid_utf8_is_error(tmp_path: Path) -> None:
|
|
|
221
171
|
md_file.write_bytes(b"- [x] caf\xe9\n")
|
|
222
172
|
item = parse_file(md_file)
|
|
223
173
|
assert item.status == "error"
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def test_file_empty_progress_value_is_error(tmp_path: Path) -> None:
|
|
227
|
-
md_file = tmp_path / "p.md"
|
|
228
|
-
md_file.write_text("---\nprogress:\n---\n- [x] a\n", encoding="utf-8")
|
|
229
|
-
item = parse_file(md_file)
|
|
230
|
-
assert (item.mode, item.status) == ("counter", "error")
|
|
@@ -14,11 +14,9 @@ from checkmarks.render import (
|
|
|
14
14
|
truncate,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
OK = Item(Path("books/dune.md"), "Dune", 143, 320, "pages", "
|
|
18
|
-
EMPTY = Item(
|
|
19
|
-
|
|
20
|
-
)
|
|
21
|
-
ERROR = Item(Path("bad.md"), "Bad", None, None, "units", "counter", "error", "oops")
|
|
17
|
+
OK = Item(Path("books/dune.md"), "Dune", 143, 320, "pages", "ok")
|
|
18
|
+
EMPTY = Item(Path("notes.md"), "Notes", None, None, "tasks", "empty", "no boxes")
|
|
19
|
+
ERROR = Item(Path("bad.md"), "Bad", None, None, "tasks", "error", "oops")
|
|
22
20
|
|
|
23
21
|
|
|
24
22
|
def bar_of(line: str) -> str:
|
|
@@ -30,15 +28,15 @@ def bar_of(line: str) -> str:
|
|
|
30
28
|
|
|
31
29
|
def test_tsv_rows() -> None:
|
|
32
30
|
assert render_tsv([OK, EMPTY, ERROR]).splitlines() == [
|
|
33
|
-
"Dune\t143\t320\t44.7\tpages\
|
|
34
|
-
"Notes\t\t\t\ttasks\
|
|
35
|
-
"Bad\t\t\t\
|
|
31
|
+
"Dune\t143\t320\t44.7\tpages\tok\tbooks/dune.md",
|
|
32
|
+
"Notes\t\t\t\ttasks\tempty\tnotes.md",
|
|
33
|
+
"Bad\t\t\t\ttasks\terror\tbad.md",
|
|
36
34
|
]
|
|
37
35
|
|
|
38
36
|
|
|
39
37
|
def test_tsv_cleans_title_and_unit() -> None:
|
|
40
|
-
item = Item(Path("a.md"), "A\tB\nC\r", 1, 2, "x\ty", "
|
|
41
|
-
assert render_tsv([item]) == "A B C \t1\t2\t50.0\tx y\
|
|
38
|
+
item = Item(Path("a.md"), "A\tB\nC\r", 1, 2, "x\ty", "ok")
|
|
39
|
+
assert render_tsv([item]) == "A B C \t1\t2\t50.0\tx y\tok\ta.md\n"
|
|
42
40
|
|
|
43
41
|
|
|
44
42
|
def test_tsv_empty_list() -> None:
|
|
@@ -56,7 +54,6 @@ def test_json_rows() -> None:
|
|
|
56
54
|
"total": 320,
|
|
57
55
|
"pct": 44.7,
|
|
58
56
|
"unit": "pages",
|
|
59
|
-
"mode": "counter",
|
|
60
57
|
"status": "ok",
|
|
61
58
|
"path": "books/dune.md",
|
|
62
59
|
}
|
|
@@ -66,7 +63,7 @@ def test_json_rows() -> None:
|
|
|
66
63
|
|
|
67
64
|
|
|
68
65
|
def test_json_keeps_unicode() -> None:
|
|
69
|
-
item = Item(Path("g.md"), "Gödel", 1, 2, "pages", "
|
|
66
|
+
item = Item(Path("g.md"), "Gödel", 1, 2, "pages", "ok")
|
|
70
67
|
assert "Gödel" in render_json([item])
|
|
71
68
|
|
|
72
69
|
|
|
@@ -106,11 +103,11 @@ def test_pretty_pct(done: int, total: int, expected: int) -> None:
|
|
|
106
103
|
|
|
107
104
|
def test_pretty_spec_example() -> None:
|
|
108
105
|
items = [
|
|
109
|
-
Item(Path("a"), "Thinking, Fast and Slow", 143, 320, "pages", "
|
|
110
|
-
Item(Path("b"), "Gödel, Escher, Bach", 180, 777, "pages", "
|
|
111
|
-
Item(Path("c"), "Rust tutorial", 12, 28, "tasks", "
|
|
112
|
-
Item(Path("d"), "NN hands-on", 2, 8, "tasks", "
|
|
113
|
-
Item(Path("e"), "Broken file", None, None, "
|
|
106
|
+
Item(Path("a"), "Thinking, Fast and Slow", 143, 320, "pages", "ok"),
|
|
107
|
+
Item(Path("b"), "Gödel, Escher, Bach", 180, 777, "pages", "ok"),
|
|
108
|
+
Item(Path("c"), "Rust tutorial", 12, 28, "tasks", "ok"),
|
|
109
|
+
Item(Path("d"), "NN hands-on", 2, 8, "tasks", "ok"),
|
|
110
|
+
Item(Path("e"), "Broken file", None, None, "tasks", "error"),
|
|
114
111
|
]
|
|
115
112
|
assert render_pretty(items, 50, False).splitlines() == [
|
|
116
113
|
"Thinking, Fast… [████------] 143/320 pages 45%",
|
|
@@ -126,7 +123,7 @@ def test_pretty_lines_fit_width() -> None:
|
|
|
126
123
|
OK,
|
|
127
124
|
EMPTY,
|
|
128
125
|
ERROR,
|
|
129
|
-
Item(Path("w"), "日本語の本" * 5, 1, 2, "p", "
|
|
126
|
+
Item(Path("w"), "日本語の本" * 5, 1, 2, "p", "ok"),
|
|
130
127
|
]
|
|
131
128
|
for width in (50, 60, 80):
|
|
132
129
|
lines = render_pretty(items, width, False).splitlines()
|
|
@@ -134,7 +131,7 @@ def test_pretty_lines_fit_width() -> None:
|
|
|
134
131
|
|
|
135
132
|
|
|
136
133
|
def test_pretty_columns_align() -> None:
|
|
137
|
-
items = [OK, EMPTY, ERROR, Item(Path("w"), "日本", 1, 2, "p", "
|
|
134
|
+
items = [OK, EMPTY, ERROR, Item(Path("w"), "日本", 1, 2, "p", "ok")]
|
|
138
135
|
lines = render_pretty(items, 80, False).splitlines()
|
|
139
136
|
starts = {display_width(line[: line.index("[")]) for line in lines}
|
|
140
137
|
widths = {display_width(line) for line in lines}
|
|
@@ -153,7 +150,7 @@ def test_pretty_bar_min_width() -> None:
|
|
|
153
150
|
|
|
154
151
|
|
|
155
152
|
def test_pretty_bar_is_floored() -> None:
|
|
156
|
-
item = Item(Path("a"), "A", 319, 320, "pages", "
|
|
153
|
+
item = Item(Path("a"), "A", 319, 320, "pages", "ok")
|
|
157
154
|
line = render_pretty([item], 200, False).splitlines()[0]
|
|
158
155
|
assert bar_of(line) == "█" * 39 + "-"
|
|
159
156
|
assert line.endswith(" 99%")
|
|
@@ -173,14 +170,14 @@ def test_pretty_only_non_ok_rows() -> None:
|
|
|
173
170
|
|
|
174
171
|
|
|
175
172
|
def test_pretty_color_only_when_complete() -> None:
|
|
176
|
-
complete = Item(Path("a"), "A", 5, 5, "tasks", "
|
|
173
|
+
complete = Item(Path("a"), "A", 5, 5, "tasks", "ok")
|
|
177
174
|
out = render_pretty([OK, complete, ERROR], 60, True).splitlines()
|
|
178
175
|
assert out[1].startswith(GREEN)
|
|
179
176
|
assert "\033[" not in out[0] + out[2]
|
|
180
177
|
|
|
181
178
|
|
|
182
179
|
def test_pretty_no_color() -> None:
|
|
183
|
-
complete = Item(Path("a"), "A", 5, 5, "tasks", "
|
|
180
|
+
complete = Item(Path("a"), "A", 5, 5, "tasks", "ok")
|
|
184
181
|
assert "\033[" not in render_pretty([complete], 60, False)
|
|
185
182
|
|
|
186
183
|
|
|
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
|