checkmarks 0.2.1__tar.gz → 0.4.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.
Files changed (26) hide show
  1. {checkmarks-0.2.1/src/checkmarks.egg-info → checkmarks-0.4.0}/PKG-INFO +33 -47
  2. {checkmarks-0.2.1 → checkmarks-0.4.0}/README.md +31 -24
  3. {checkmarks-0.2.1 → checkmarks-0.4.0}/pyproject.toml +4 -3
  4. {checkmarks-0.2.1 → checkmarks-0.4.0}/src/checkmarks/cli.py +3 -1
  5. {checkmarks-0.2.1 → checkmarks-0.4.0}/src/checkmarks/collect.py +10 -7
  6. {checkmarks-0.2.1 → checkmarks-0.4.0}/src/checkmarks/parse.py +2 -29
  7. {checkmarks-0.2.1 → checkmarks-0.4.0}/src/checkmarks/render.py +0 -2
  8. {checkmarks-0.2.1 → checkmarks-0.4.0/src/checkmarks.egg-info}/PKG-INFO +33 -47
  9. {checkmarks-0.2.1 → checkmarks-0.4.0}/src/checkmarks.egg-info/SOURCES.txt +0 -2
  10. {checkmarks-0.2.1 → checkmarks-0.4.0}/tests/test_cli.py +20 -7
  11. {checkmarks-0.2.1 → checkmarks-0.4.0}/tests/test_collect.py +8 -1
  12. {checkmarks-0.2.1 → checkmarks-0.4.0}/tests/test_parse.py +5 -62
  13. {checkmarks-0.2.1 → checkmarks-0.4.0}/tests/test_render.py +19 -22
  14. checkmarks-0.2.1/tests/fixtures/bad_progress.md +0 -4
  15. checkmarks-0.2.1/tests/fixtures/counter.md +0 -7
  16. {checkmarks-0.2.1 → checkmarks-0.4.0}/LICENSE +0 -0
  17. {checkmarks-0.2.1 → checkmarks-0.4.0}/MANIFEST.in +0 -0
  18. {checkmarks-0.2.1 → checkmarks-0.4.0}/setup.cfg +0 -0
  19. {checkmarks-0.2.1 → checkmarks-0.4.0}/src/checkmarks/__init__.py +0 -0
  20. {checkmarks-0.2.1 → checkmarks-0.4.0}/src/checkmarks.egg-info/dependency_links.txt +0 -0
  21. {checkmarks-0.2.1 → checkmarks-0.4.0}/src/checkmarks.egg-info/entry_points.txt +0 -0
  22. {checkmarks-0.2.1 → checkmarks-0.4.0}/src/checkmarks.egg-info/top_level.txt +0 -0
  23. {checkmarks-0.2.1 → checkmarks-0.4.0}/tests/fixtures/empty.md +0 -0
  24. {checkmarks-0.2.1 → checkmarks-0.4.0}/tests/fixtures/heading_in_fence.md +0 -0
  25. {checkmarks-0.2.1 → checkmarks-0.4.0}/tests/fixtures/quoted_title.md +0 -0
  26. {checkmarks-0.2.1 → checkmarks-0.4.0}/tests/fixtures/unclosed_frontmatter.md +0 -0
@@ -1,30 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: checkmarks
3
- Version: 0.2.1
3
+ Version: 0.4.0
4
4
  Summary: Progress dashboard for Markdown checklists
5
5
  Author-email: menisadi <menisadi@gmail.com>
6
- License: MIT License
7
-
8
- Copyright (c) 2025 Meni
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
6
+ License-Expression: MIT
28
7
  Project-URL: Homepage, https://github.com/menisadi/checkmarks
29
8
  Project-URL: Issues, https://github.com/menisadi/checkmarks/issues
30
9
  Keywords: cli,markdown,checklists
@@ -39,10 +18,10 @@ A small command-line dashboard for your progress. Point it at a folder of Markdo
39
18
 
40
19
  ```
41
20
  $ checkmarks ~/progress
42
- Thinking, Fast and Slow [████------] 143/320 pages 45%
43
- Gödel, Escher, Bach [██--------] 180/777 pages 23%
44
- Rust tutorial [████------] 12/28 tasks 43%
45
- NN hands-on [██--------] 2/8 tasks 25%
21
+ Thinking, Fast and Slow [████------] 17/38 chapters 45%
22
+ Gödel, Escher, Bach [██--------] 5/22 chapters 23%
23
+ Rust tutorial [████------] 12/28 tasks 43%
24
+ NN hands-on [██--------] 2/8 tasks 25%
46
25
  ```
47
26
 
48
27
  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 +75,30 @@ By default, checkmarks counts checkboxes:
96
75
  - List items can start with `-`, `*`, `+`, `1.` or `1)`.
97
76
  - Checkboxes inside fenced code blocks are ignored.
98
77
 
99
- ### Counters
78
+ ### Big things: break them down
100
79
 
101
- For things that aren't a list of tasks, like the pages of a book, add frontmatter with a `progress` key. It replaces the checkbox count:
80
+ 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
81
 
103
82
  ```markdown
104
83
  ---
105
- progress: 143/320
106
- unit: pages
84
+ unit: chunks
107
85
  ---
108
86
  # Dune
87
+
88
+ - [x] pp. 1–40
89
+ - [x] pp. 41–80
90
+ - [ ] pp. 81–120
91
+ - [ ] pp. 121–160
109
92
  ```
110
93
 
111
- Supported keys:
94
+ Supported frontmatter keys:
112
95
 
113
- | Key | Meaning |
114
- |------------|--------------------------------------------------------------------|
115
- | `progress` | `done/total`, e.g. `143/320` |
116
- | `unit` | Label after the count (default `tasks`, or `units` with `progress`) |
117
- | `title` | Row title |
96
+ | Key | Meaning |
97
+ |---------|------------------------------------------|
98
+ | `unit` | Label after the count (default `tasks`) |
99
+ | `title` | Row title |
118
100
 
119
- `unit` and `title` also work in checklist files. Other keys are ignored.
101
+ Other keys are ignored.
120
102
 
121
103
  ### Titles
122
104
 
@@ -129,7 +111,7 @@ On a terminal, checkmarks prints the aligned table shown above. Rows at 100% are
129
111
  When output is piped, it switches to tab-separated rows with no header:
130
112
 
131
113
  ```
132
- title done total pct unit mode status path
114
+ title done total pct unit status path
133
115
  ```
134
116
 
135
117
  Use `--json` for a JSON array:
@@ -139,27 +121,26 @@ $ checkmarks --json dune.md
139
121
  [
140
122
  {
141
123
  "title": "Dune",
142
- "done": 143,
143
- "total": 320,
144
- "pct": 44.7,
145
- "unit": "pages",
146
- "mode": "counter",
124
+ "done": 9,
125
+ "total": 22,
126
+ "pct": 40.9,
127
+ "unit": "chapters",
147
128
  "status": "ok",
148
129
  "path": "dune.md"
149
130
  }
150
131
  ]
151
132
  ```
152
133
 
153
- `status` is `ok`, `empty` (no checkboxes) or `error` (e.g. an invalid `progress` value). Problems are reported on stderr, and the row is still shown.
134
+ `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
135
 
155
- Empty files found in folders are hidden; pass `--all` to show them. Files you name directly are always shown.
136
+ Empty files found in folders or read from stdin are hidden; pass `--all` to show them. Files you name as arguments are always shown.
156
137
 
157
138
  ## Options
158
139
 
159
140
  | Option | Description |
160
141
  |------------------------------|---------------------------------------------|
161
142
  | `-r`, `--recursive` | Descend into subfolders |
162
- | `--all` | Show empty files found in folders |
143
+ | `--all` | Show empty files from folders or stdin |
163
144
  | `--format {pretty,tsv,json}` | Choose the output format |
164
145
  | `--json` | Same as `--format json` |
165
146
  | `--no-color` | Disable colour |
@@ -167,6 +148,11 @@ Empty files found in folders are hidden; pass `--all` to show them. Files you na
167
148
 
168
149
  Exit code is `0` if at least one file has progress to show, `1` if none does, and `2` for usage errors.
169
150
 
151
+ ## Upgrading from 0.2
152
+
153
+ - The `progress: N/M` frontmatter key (counter mode) is gone. Replace it with a checklist of chapters or page ranges.
154
+ - The `mode` column (TSV) and field (JSON) are removed.
155
+
170
156
  ## Upgrading from 0.1
171
157
 
172
158
  - 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 [████------] 143/320 pages 45%
8
- Gödel, Escher, Bach [██--------] 180/777 pages 23%
9
- Rust tutorial [████------] 12/28 tasks 43%
10
- NN hands-on [██--------] 2/8 tasks 25%
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
- ### Counters
64
+ ### Big things: break them down
65
65
 
66
- For things that aren't a list of tasks, like the pages of a book, add frontmatter with a `progress` key. It replaces the checkbox count:
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
- progress: 143/320
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 | Meaning |
79
- |------------|--------------------------------------------------------------------|
80
- | `progress` | `done/total`, e.g. `143/320` |
81
- | `unit` | Label after the count (default `tasks`, or `units` with `progress`) |
82
- | `title` | Row title |
82
+ | Key | Meaning |
83
+ |---------|------------------------------------------|
84
+ | `unit` | Label after the count (default `tasks`) |
85
+ | `title` | Row title |
83
86
 
84
- `unit` and `title` also work in checklist files. Other keys are ignored.
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 mode status path
100
+ title done total pct unit status path
98
101
  ```
99
102
 
100
103
  Use `--json` for a JSON array:
@@ -104,27 +107,26 @@ $ checkmarks --json dune.md
104
107
  [
105
108
  {
106
109
  "title": "Dune",
107
- "done": 143,
108
- "total": 320,
109
- "pct": 44.7,
110
- "unit": "pages",
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` (e.g. an invalid `progress` value). Problems are reported on stderr, and the row is still shown.
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
- Empty files found in folders are hidden; pass `--all` to show them. Files you name directly are always shown.
122
+ Empty files found in folders or read from stdin are hidden; pass `--all` to show them. Files you name as arguments are always shown.
121
123
 
122
124
  ## Options
123
125
 
124
126
  | Option | Description |
125
127
  |------------------------------|---------------------------------------------|
126
128
  | `-r`, `--recursive` | Descend into subfolders |
127
- | `--all` | Show empty files found in folders |
129
+ | `--all` | Show empty files from folders or stdin |
128
130
  | `--format {pretty,tsv,json}` | Choose the output format |
129
131
  | `--json` | Same as `--format json` |
130
132
  | `--no-color` | Disable colour |
@@ -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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "checkmarks"
3
- version = "0.2.1"
3
+ version = "0.4.0"
4
4
  description = "Progress dashboard for Markdown checklists"
5
5
  authors = [
6
6
  { name = "menisadi", email = "menisadi@gmail.com" }
@@ -8,7 +8,8 @@ authors = [
8
8
 
9
9
  readme = "README.md"
10
10
 
11
- license = { file = "LICENSE" }
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
12
13
 
13
14
  keywords = ["cli", "markdown", "checklists"]
14
15
 
@@ -27,5 +28,5 @@ checkmarks = "checkmarks.cli:main"
27
28
  dev = ["pytest", "ruff", "ty"]
28
29
 
29
30
  [build-system]
30
- requires = ["setuptools>=61"]
31
+ requires = ["setuptools>=77"]
31
32
  build-backend = "setuptools.build_meta"
@@ -23,7 +23,9 @@ def build_parser() -> argparse.ArgumentParser:
23
23
  "-r", "--recursive", action="store_true", help="descend into subdirectories"
24
24
  )
25
25
  parser.add_argument(
26
- "--all", action="store_true", help="show empty files found in directories"
26
+ "--all",
27
+ action="store_true",
28
+ help="show empty files found in directories or read from stdin",
27
29
  )
28
30
  fmt = parser.add_mutually_exclusive_group()
29
31
  fmt.add_argument(
@@ -30,17 +30,19 @@ def expand_dir(directory: Path, recursive: bool) -> list[Path]:
30
30
  return sorted(found)
31
31
 
32
32
 
33
- def iter_args(paths: list[str], stdin: TextIO, env: Mapping[str, str]) -> Iterator[str]:
34
- """Yields path arguments, expanding `-` and applying the no-argument default."""
33
+ def iter_args(
34
+ paths: list[str], stdin: TextIO, env: Mapping[str, str]
35
+ ) -> Iterator[tuple[str, bool]]:
36
+ """Yields (path, from_stdin) pairs, expanding `-` and the no-argument default."""
35
37
  if not paths:
36
- yield env.get("CHECKMARKS_DIR") or "."
38
+ yield env.get("CHECKMARKS_DIR") or ".", False
37
39
  return
38
40
 
39
41
  for arg in paths:
40
42
  if arg == "-":
41
- yield from (line.strip() for line in stdin if line.strip())
43
+ yield from ((line.strip(), True) for line in stdin if line.strip())
42
44
  else:
43
- yield arg
45
+ yield arg, False
44
46
 
45
47
 
46
48
  def collect(
@@ -61,13 +63,14 @@ def collect(
61
63
  seen[key] = source
62
64
  sources.append(source)
63
65
 
64
- for arg in iter_args(paths, stdin, env):
66
+ for arg, from_stdin in iter_args(paths, stdin, env):
65
67
  path = Path(arg)
66
68
  if path.is_dir():
67
69
  for file in expand_dir(path, recursive):
68
70
  add(file, explicit=False)
69
71
  elif path.exists():
70
- add(path, explicit=True)
72
+ # Piped paths are usually a bulk listing, so treat them like a folder
73
+ add(path, explicit=not from_stdin)
71
74
  else:
72
75
  warnings.append(f"{arg}: no such file or directory")
73
76
 
@@ -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
- path, title, None, None, unit, "checklist", "empty", "no checkboxes found"
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,30 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: checkmarks
3
- Version: 0.2.1
3
+ Version: 0.4.0
4
4
  Summary: Progress dashboard for Markdown checklists
5
5
  Author-email: menisadi <menisadi@gmail.com>
6
- License: MIT License
7
-
8
- Copyright (c) 2025 Meni
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
6
+ License-Expression: MIT
28
7
  Project-URL: Homepage, https://github.com/menisadi/checkmarks
29
8
  Project-URL: Issues, https://github.com/menisadi/checkmarks/issues
30
9
  Keywords: cli,markdown,checklists
@@ -39,10 +18,10 @@ A small command-line dashboard for your progress. Point it at a folder of Markdo
39
18
 
40
19
  ```
41
20
  $ checkmarks ~/progress
42
- Thinking, Fast and Slow [████------] 143/320 pages 45%
43
- Gödel, Escher, Bach [██--------] 180/777 pages 23%
44
- Rust tutorial [████------] 12/28 tasks 43%
45
- NN hands-on [██--------] 2/8 tasks 25%
21
+ Thinking, Fast and Slow [████------] 17/38 chapters 45%
22
+ Gödel, Escher, Bach [██--------] 5/22 chapters 23%
23
+ Rust tutorial [████------] 12/28 tasks 43%
24
+ NN hands-on [██--------] 2/8 tasks 25%
46
25
  ```
47
26
 
48
27
  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 +75,30 @@ By default, checkmarks counts checkboxes:
96
75
  - List items can start with `-`, `*`, `+`, `1.` or `1)`.
97
76
  - Checkboxes inside fenced code blocks are ignored.
98
77
 
99
- ### Counters
78
+ ### Big things: break them down
100
79
 
101
- For things that aren't a list of tasks, like the pages of a book, add frontmatter with a `progress` key. It replaces the checkbox count:
80
+ 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
81
 
103
82
  ```markdown
104
83
  ---
105
- progress: 143/320
106
- unit: pages
84
+ unit: chunks
107
85
  ---
108
86
  # Dune
87
+
88
+ - [x] pp. 1–40
89
+ - [x] pp. 41–80
90
+ - [ ] pp. 81–120
91
+ - [ ] pp. 121–160
109
92
  ```
110
93
 
111
- Supported keys:
94
+ Supported frontmatter keys:
112
95
 
113
- | Key | Meaning |
114
- |------------|--------------------------------------------------------------------|
115
- | `progress` | `done/total`, e.g. `143/320` |
116
- | `unit` | Label after the count (default `tasks`, or `units` with `progress`) |
117
- | `title` | Row title |
96
+ | Key | Meaning |
97
+ |---------|------------------------------------------|
98
+ | `unit` | Label after the count (default `tasks`) |
99
+ | `title` | Row title |
118
100
 
119
- `unit` and `title` also work in checklist files. Other keys are ignored.
101
+ Other keys are ignored.
120
102
 
121
103
  ### Titles
122
104
 
@@ -129,7 +111,7 @@ On a terminal, checkmarks prints the aligned table shown above. Rows at 100% are
129
111
  When output is piped, it switches to tab-separated rows with no header:
130
112
 
131
113
  ```
132
- title done total pct unit mode status path
114
+ title done total pct unit status path
133
115
  ```
134
116
 
135
117
  Use `--json` for a JSON array:
@@ -139,27 +121,26 @@ $ checkmarks --json dune.md
139
121
  [
140
122
  {
141
123
  "title": "Dune",
142
- "done": 143,
143
- "total": 320,
144
- "pct": 44.7,
145
- "unit": "pages",
146
- "mode": "counter",
124
+ "done": 9,
125
+ "total": 22,
126
+ "pct": 40.9,
127
+ "unit": "chapters",
147
128
  "status": "ok",
148
129
  "path": "dune.md"
149
130
  }
150
131
  ]
151
132
  ```
152
133
 
153
- `status` is `ok`, `empty` (no checkboxes) or `error` (e.g. an invalid `progress` value). Problems are reported on stderr, and the row is still shown.
134
+ `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
135
 
155
- Empty files found in folders are hidden; pass `--all` to show them. Files you name directly are always shown.
136
+ Empty files found in folders or read from stdin are hidden; pass `--all` to show them. Files you name as arguments are always shown.
156
137
 
157
138
  ## Options
158
139
 
159
140
  | Option | Description |
160
141
  |------------------------------|---------------------------------------------|
161
142
  | `-r`, `--recursive` | Descend into subfolders |
162
- | `--all` | Show empty files found in folders |
143
+ | `--all` | Show empty files from folders or stdin |
163
144
  | `--format {pretty,tsv,json}` | Choose the output format |
164
145
  | `--json` | Same as `--format json` |
165
146
  | `--no-color` | Disable colour |
@@ -167,6 +148,11 @@ Empty files found in folders are hidden; pass `--all` to show them. Files you na
167
148
 
168
149
  Exit code is `0` if at least one file has progress to show, `1` if none does, and `2` for usage errors.
169
150
 
151
+ ## Upgrading from 0.2
152
+
153
+ - The `progress: N/M` frontmatter key (counter mode) is gone. Replace it with a checklist of chapters or page ranges.
154
+ - The `mode` column (TSV) and field (JSON) are removed.
155
+
170
156
  ## Upgrading from 0.1
171
157
 
172
158
  - Output is now a table row per file instead of the old "📊 Progress for:" block.
@@ -16,8 +16,6 @@ tests/test_cli.py
16
16
  tests/test_collect.py
17
17
  tests/test_parse.py
18
18
  tests/test_render.py
19
- tests/fixtures/bad_progress.md
20
- tests/fixtures/counter.md
21
19
  tests/fixtures/empty.md
22
20
  tests/fixtures/heading_in_fence.md
23
21
  tests/fixtures/quoted_title.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
- "---\nprogress: 5/5\n---\n# Beta\n", encoding="utf-8"
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").write_text("---\nprogress: x\n---\n", encoding="utf-8")
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\tchecklist\tok\ta.md",
35
- "Beta\t5\t5\t100.0\tunits\tcounter\tok\tb.md",
36
- "bad\t\t\t\tunits\tcounter\terror\tbad.md",
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 == "checkmarks: bad.md: unparseable progress: 'x'\n"
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\tchecklist\tempty\tnotes.md" in capsys.readouterr().out
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:
@@ -127,6 +127,19 @@ def test_stdin_paths(
127
127
  assert capsys.readouterr().out.startswith("Beta\t")
128
128
 
129
129
 
130
+ def test_stdin_hides_empty_unless_all(
131
+ tree: Path, capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch
132
+ ) -> None:
133
+ import io
134
+
135
+ monkeypatch.setattr(sys, "stdin", io.StringIO("a.md\nnotes.md\n"))
136
+ main(["-"])
137
+ assert "Notes" not in capsys.readouterr().out
138
+ monkeypatch.setattr(sys, "stdin", io.StringIO("a.md\nnotes.md\n"))
139
+ main(["-", "--all"])
140
+ assert "Notes\t\t\t\ttasks\tempty\tnotes.md" in capsys.readouterr().out
141
+
142
+
130
143
  def test_env_dir(
131
144
  tree: Path,
132
145
  tmp_path_factory: pytest.TempPathFactory,
@@ -68,11 +68,18 @@ def test_stdin_paths(tmp_path: Path) -> None:
68
68
  stdin = io.StringIO(f"{tmp_path / 'a.md'}\n\n \n{tmp_path / 'dir'}\n")
69
69
  sources, _ = collect(["-"], False, stdin, {})
70
70
  assert [(n, s.explicit) for n, s in zip(names(sources, tmp_path), sources)] == [
71
- ("a.md", True),
71
+ ("a.md", False),
72
72
  ("dir/b.md", False),
73
73
  ]
74
74
 
75
75
 
76
+ def test_stdin_path_explicit_when_also_an_arg(tmp_path: Path) -> None:
77
+ make_tree(tmp_path, ["a.md"])
78
+ stdin = io.StringIO(f"{tmp_path / 'a.md'}\n")
79
+ sources, _ = collect(["-", str(tmp_path / "a.md")], False, stdin, {})
80
+ assert sources == [Source(tmp_path / "a.md", True)]
81
+
82
+
76
83
  def test_stdin_not_read_without_dash(
77
84
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
78
85
  ) -> 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(["---", "progress: 1/2", "---", "# T"])
97
- assert meta == {"progress": "1/2"}
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, item.mode) == (1, 2, "checklist")
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.mode, item.status, item.done, item.total) == (
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.mode, item.status, item.title) == ("checklist", "error", "missing")
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", "counter", "ok")
18
- EMPTY = Item(
19
- Path("notes.md"), "Notes", None, None, "tasks", "checklist", "empty", "no boxes"
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\tcounter\tok\tbooks/dune.md",
34
- "Notes\t\t\t\ttasks\tchecklist\tempty\tnotes.md",
35
- "Bad\t\t\t\tunits\tcounter\terror\tbad.md",
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", "checklist", "ok")
41
- assert render_tsv([item]) == "A B C \t1\t2\t50.0\tx y\tchecklist\tok\ta.md\n"
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", "counter", "ok")
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", "counter", "ok"),
110
- Item(Path("b"), "Gödel, Escher, Bach", 180, 777, "pages", "counter", "ok"),
111
- Item(Path("c"), "Rust tutorial", 12, 28, "tasks", "checklist", "ok"),
112
- Item(Path("d"), "NN hands-on", 2, 8, "tasks", "checklist", "ok"),
113
- Item(Path("e"), "Broken file", None, None, "units", "counter", "error"),
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", "counter", "ok"),
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", "counter", "ok")]
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", "counter", "ok")
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", "checklist", "ok")
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", "checklist", "ok")
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
 
@@ -1,4 +0,0 @@
1
- ---
2
- progress: about half
3
- ---
4
- # Bad progress
@@ -1,7 +0,0 @@
1
- ---
2
- progress: 143 / 320
3
- unit: pages
4
- ---
5
- # Dune
6
- - [x] Book One
7
- - [ ] Book Two
File without changes
File without changes
File without changes