The first release since 0.3.2, collecting everything built since March.
Highlights
- New options:
--no-saveuses setup's choices for one run without writing.tally-config.toml(#5),--exclude PATTERNleaves files out of one run (#10), and--exclude-rust-tests/--no-exclude-rust-testsoverride the Rust setting for one run [exclude] patterns: gitignore-style patterns for files a directory exclude can't reach (#6)[rust] exclude_test_modules: leaves inline#[cfg(test)]code out of the count, with anrtoggle in setup (#8)- Configs: every
.tally-config.tomlfrom the git root down to the scanned directory applies;[specs] ignoreoverrides spec detection - Gitignore: follows git's rules entry by entry, including
info/excludein worktrees and submodules, and nested repos are counted with their own rules - Languages: MATLAB (#4) and Solidity, for 44 in all
- Fixes and speed: many fixes to the image card, setup TUI, encodings and line endings; named pipes no longer hang a scan; a plain run starts about twice as fast
Full details below.
0.4.3 - 2026-11-11
Added
- MATLAB language support for
.mfiles, counted as Code with%single-line comment detection (issue #4).%{ ... %}block markers count as comments and the lines between them as code, like/* */in other languages. MATLAB's binary and zip formats (.mat,.fig,.mlx,.mlapp,.mltbx,.mlprj) aren't source and aren't counted. - Files:
src/tallyman/languages.py,tests/test_languages.py,README.md,CLAUDE.md,pyproject.toml,uv.lock - New
--no-saveruns Tallyman without leaving a.tally-config.tomlbehind (issue #5). Setup opens as usual on a first run, or with--setup. Its button reads Run instead of Save & Run, a subtitle says nothing is saved, and the choices apply to that run only, so the next run opens setup again. Configs that already apply are still read and never changed, sotallyman --setup --no-savetries other settings without touching the config. A malformed config there is skipped with a warning, as--setupdoes, and left as it is. - Files:
src/tallyman/cli.py,src/tallyman/tui/setup_app.py,tests/test_cli.py,tests/test_setup_app.py,README.md,CLAUDE.md,.cursor/rules/coding-guidelines.mdc,.cursor/rules/project-rules.mdc,plans/006-tallyman-architect.plan.md,pyproject.toml,uv.lock - Plan:
plans/015-no-save.plan.md - New
--exclude PATTERNleaves files out of one run without editing.tally-config.toml(issue #10), for exampletallyman --exclude 'tests/' --exclude 'tests.rs'to see a project's size without its tests. Each value is one gitignore-style pattern, matched from the scanned directory, and the flag can be repeated. The patterns are the last word over every config's[exclude] patterns, nested configs included, and a!brings back what a config pattern left out (never a gitignored, hidden or[exclude] directoriesentry). They are never saved, setup doesn't show them, and what they remove is counted in theExcluded by [exclude] patternsline. A pattern that can never match (blank, a comment such as#gen, a lone!or/, one spanning lines, a./src/gen/-style path, or an absolute path into the scanned directory) stops the run with exit code 2, suggesting the fix where there is one. - Files:
src/tallyman/cli.py,src/tallyman/gitignore.py,src/tallyman/walker.py,tests/test_cli.py,tests/test_gitignore.py,tests/test_walker.py,README.md,CLAUDE.md,.cursor/rules/coding-guidelines.mdc,.cursor/rules/project-rules.mdc,plans/006-tallyman-architect.plan.md,pyproject.toml,uv.lock - Plan:
plans/014-one-off-overrides/phase-01-exclude-flag.plan.md - New
--exclude-rust-testsand--no-exclude-rust-testsleave Rust test code out of one run, or count it, whatever[rust] exclude_test_modulessays in any config, nested crate configs included. Neither is saved, and setup shows and saves the config's value; the count after setup uses the flag. Given both, the last one wins. - Files:
src/tallyman/cli.py,src/tallyman/walker.py,tests/test_cli.py,tests/test_walker.py,README.md,CLAUDE.md,.cursor/rules/coding-guidelines.mdc,.cursor/rules/project-rules.mdc,plans/006-tallyman-architect.plan.md - Plan:
plans/014-one-off-overrides/phase-02-rust-flag.plan.md - New
[rust] exclude_test_modules = trueconfig key leaves Rust test-only code out of the count, so Tallyman measures the code that ships (issue #8). It drops any item marked#[cfg(test)]or#[cfg(all(test, ...))](a test module, helper fn,use,impl, or a test-only method inside animplortrait), with its attributes and doc comments, and whole files or modules under#![cfg(test)]. Code after a test module still counts.cfg(any(test, ...))isn't test-only and stays. Off by default. It follows the config chain like every other setting: a nested config (one per crate) can turn it on or off for its subtree, andfalseoverrides a parent'strue. Setup keeps the value when it rewrites the config. A file Tallyman can't follow (unbalanced brackets, an unterminated string) is counted in full. - A Rust-aware scanner finds the test-only items: it skips brackets inside strings, raw strings, char literals and nested comments, tells char literals from lifetimes, and pairs every bracket before trusting any of them.
- Files:
src/tallyman/rust_tests.py,tests/test_rust_tests.py - Plan:
plans/013-rust-test-code/phase-01-scanner.plan.md - The setting reaches the counter through the config chain, nested configs and the walker, and
--setupkeeps it. - Files:
src/tallyman/config.py,src/tallyman/walker.py,src/tallyman/counter.py,src/tallyman/aggregator.py,src/tallyman/languages.py,src/tallyman/cli.py,src/tallyman/tui/setup_app.py,tests/test_config.py,tests/test_walker.py,tests/test_counter.py,tests/test_aggregator.py,tests/test_cli.py,tests/test_setup_app.py - Plan:
plans/013-rust-test-code/phase-02-config-and-counting.plan.md --setupshows aRust test code: counted(orexcluded) line when the project has Rust code, andrswitches it. It saves the setting in the scanned directory's config only when it differs from what parent configs give, so pressingrtwice changes nothing. Without Rust code, the line and the key don't appear.- Files:
src/tallyman/tui/setup_app.py,src/tallyman/config.py,src/tallyman/cli.py,tests/test_setup_app.py,tests/test_config.py,tests/test_cli.py,README.md,CLAUDE.md - Plan:
plans/013-rust-test-code/phase-04-setup-toggle.plan.md - When Rust test code is left out, the report ends with a dim line such as
Excluded Rust test code: 2,412 lines in 37 files, after the patterns line and under the same blank line, also shown when nothing else was counted. A file of only test code isn't counted as a Rust file. The image card shows the reduced numbers and no extra line. - README and
CLAUDE.mddescribe the option. - Files:
src/tallyman/display.py,tests/test_display.py,tests/test_cli.py,README.md,CLAUDE.md,.cursor/rules/project-rules.mdc,.cursor/rules/coding-guidelines.mdc,plans/006-tallyman-architect.plan.md,pyproject.toml,uv.lock - Plan:
plans/013-rust-test-code/phase-03-report-and-docs.plan.md - New
[exclude] patterns = [...]config key: gitignore-style patterns, matched relative to the directory of the config that declares them, for files a directory exclude can't reach, such as Rust'ssrc/routing/tests.rsbesidesrc/routing.rs(issue #6). A pattern without a slash matches at any depth, a trailing/matches directories, and a deeper config's!patterncan bring back what a shallower config's pattern excluded. Patterns never bring back gitignored files, excluded or hidden directories, or the default skips, and a parent config's patterns never exclude the scanned directory itself. Setup shows directories a pattern excludes as locked and keeps the patterns, in order, when it rewrites the config. - Files:
src/tallyman/config.py,src/tallyman/gitignore.py,src/tallyman/walker.py,src/tallyman/cli.py,src/tallyman/tui/setup_app.py,tests/test_config.py,tests/test_gitignore.py,tests/test_walker.py,tests/test_cli.py,tests/test_setup_app.py - Plan:
plans/007-exclude-patterns/phase-02-exclude-patterns.plan.md - When
[exclude] patternsremove anything, the report ends with a dim line such asExcluded by [exclude] patterns: 12 files, 1 directory, also shown when nothing else was counted. The image card is unchanged. - README and
CLAUDE.mddescribe the merged config chain and patterns. - Files:
src/tallyman/display.py,src/tallyman/cli.py,tests/test_display.py,tests/test_cli.py,README.md,CLAUDE.md - Plan:
plans/007-exclude-patterns/phase-03-report-and-docs.plan.md - Solidity language support for
.solfiles, counted as Code with//single-line comment detection. - Files:
src/tallyman/languages.py,tests/test_languages.py,tests/test_counter.py,tests/test_walker.py,README.md - New
[specs] ignore = [...]config key: directories that are never specs, even when namedspecs,plans,specificationsoragents. It covers the whole subtree, including auto-detection; a deeper[specs] directoriesentry still wins. Pressingsin setup on an auto-detected spec directory now writes it here, so un-marking one finally sticks. - Files:
src/tallyman/config.py,src/tallyman/walker.py,src/tallyman/cli.py,src/tallyman/tui/setup_app.py,tests/test_setup_app.py,tests/test_walker.py,tests/test_config.py,tests/test_cli.py - Plan:
plans/005-hardening/phase-03-spec-overrides-and-tui.plan.md
Changed
--nono longer abbreviates--no-color: it could now also mean--no-exclude-rust-tests, so it is an error. Write--no-color(or--no-c). Likewise--exclno longer abbreviates--exclude.- The image card's legend marks each entry with a square swatch in its bar segment's colour, so a language can be matched to its block by colour rather than by position. The
·separators are gone; the terminal legend already coloured each label and is unchanged. - Files:
src/tallyman/image.py,tests/test_image.py,README.md,CLAUDE.md,plans/006-tallyman-architect.plan.md - The language list on each category line (the first three, then
, etc) comes from one function in the shared report model,category_language_list(), instead of a copy in each output. Output unchanged. - Files:
src/tallyman/aggregator.py,src/tallyman/display.py,src/tallyman/image.py,tests/test_aggregator.py,CLAUDE.md,.cursor/rules/coding-guidelines.mdc,plans/006-tallyman-architect.plan.md - Every optional type hint is
Optional[X], andtests/test_conventions.pyfails the suite on anyX | None,None | XorUnion[..., None]annotation, including quoted ones. Ruff's wholeANNrule set is on forsrc/andtests/, so every function, each test included, has full annotations, and Pyrefly treatsunnecessary-type-conversionas an error. Annotations only; nothing a user sees changes. - Files:
ruff.toml,pyrefly.toml,src/tallyman/image.py,src/tallyman/languages.py,src/tallyman/walker.py,tests/,CLAUDE.md,plans/006-tallyman-architect.plan.md - Plan:
plans/012-housekeeping/phase-01-type-hints.plan.md - R's extension list drops
.R, which never matched anything because lookup lowercases the suffix;analysis.Rstill counts as R, and a test keeps every registered extension lowercase. The README says extensions match in any case and spells out when first-run setup launches (no config in the scanned directory or above it, and a terminal), and a test checks that a parent config alone skips setup.CLAUDE.mdand.cursor/rules/describe the current code again. - Files:
src/tallyman/languages.py,tests/test_languages.py,tests/test_cli.py,README.md,CLAUDE.md,.cursor/rules/,plans/006-tallyman-architect.plan.md - Plan:
plans/012-housekeeping/phase-02-docs-and-recount.plan.md - A plain run no longer imports Textual; only setup loads it. That roughly halves start-up: importing
tallyman.cliwent from about 75 ms to about 33 ms. A test checks that a plain run, with or without a config, loads neither Textual nor Pillow. - Files:
src/tallyman/cli.py,tests/test_cli.py,CLAUDE.md - Plan:
plans/011-setup-tui/phase-01-lazy-textual-and-text-labels.plan.md - Setup reads nested configs only where the walker would: not inside gitignored, pattern-excluded or hidden directories, or ones a parent or nested config excludes. It still reads the ones beneath the scanned directory's own exclusions, since those can be lifted in setup. It no longer walks the whole tree first to find them, so setup on a project with a large gitignored
node_modulesopens without that extra walk (about 0.3 s on 12,000 ignored directories). A config the walker never reads can no longer make setup drop a scan-root entry as a copy of it. - Directories excluded by a parent or nested config are shown in setup without their contents.
- Files:
src/tallyman/tui/setup_app.py,src/tallyman/config.py,src/tallyman/cli.py,tests/test_setup_app.py,tests/test_config.py,tests/test_cli.py,README.md,CLAUDE.md,plans/006-tallyman-architect.plan.md - Plan:
plans/011-setup-tui/phase-03-nested-configs-in-populate.plan.md - Nested git repositories, including submodules and worktrees inside the tree, are still counted, but now with their own rules: inside one, only its own
.gitignorefiles andinfo/excludeapply, as if git were run there, and the outer repository's patterns stop at its root. A nested repository the outer one ignores stays out. When the scanned folder isn't itself a repository, its .gitignore likewise stops at each repository inside it.[exclude]and[specs]settings apply across repository boundaries as before. - Files:
src/tallyman/gitignore.py,src/tallyman/walker.py,src/tallyman/tui/setup_app.py,tests/test_gitignore.py,tests/test_walker.py,tests/test_setup_app.py,tests/test_cli.py,CLAUDE.md - Plan:
plans/010-git-fidelity/phase-02-nested-repos.plan.md - The image card's bar now folds languages past the fifth into Other, as its legend already did, so every coloured block on the card has a legend entry.
- A legend share that isn't zero but rounds to 0% reads
<1%, and one that rounds to 100% while something else is listed reads>99%, in the terminal and on the image. - The terminal report and the image card now get their categories, totals, bar segments, legend labels and segment widths from one report model in
aggregator.py(active_categories(),bar_segments(),segment_widths(),percent_label()), so they can't disagree; a test checks that they don't. The image no longer imports fromdisplay.py. - Files:
src/tallyman/aggregator.py,src/tallyman/languages.py,src/tallyman/display.py,src/tallyman/image.py,tests/test_aggregator.py,tests/test_display.py,tests/test_image.py,CLAUDE.md - Plan:
plans/008-output-hardening/phase-01-shared-report-model.plan.md - The image card's attribution footer links to mkennedy.codes/tools/tallyman.
- Files:
src/tallyman/image.py - Removed the
pathspecdependency; tallyman now compiles gitignore patterns itself (src/tallyman/gitignore.py). - Ruff is configured only in
ruff.toml; the ignored[tool.ruff]tables inpyproject.tomlare gone, and theWrules they listed are now on. - The
.tally-config.tomlsearch now stops at the git repo root, and outside a repo only the scanned directory is checked, so a stray config in a home directory no longer applies to every project or suppresses first-run setup. - With no config and no terminal (CI, pipes), tallyman now counts with default settings and prints a one-line note on stderr instead of launching the setup TUI and hanging. No config is written.
--setupwithout a terminal exits with status 2. - Files:
src/tallyman/config.py,src/tallyman/cli.py,tests/test_config.py,tests/test_cli.py - Plan:
plans/005-hardening/phase-02-config-handling.plan.md - Every
.tally-config.tomlfrom the git repo root down to the scanned directory now applies, instead of only the nearest one. Exclusions add up; for specs, the deeper setting wins, and on the same path the deeper config wins. Scanningrepo/appwith configs in bothrepoandrepo/appno longer silently dropsrepo's settings. --setupin a directory below another config shows that config's settings as locked ("set in a parent config") and no longer copies them into the new config. Copies written by earlier versions are dropped the next time setup saves.- Nested configs below the scanned directory also resolve a spec/ignore clash on the same path in favour of the deeper config. Before, the spec entry always won.
- In setup,
xnow works on a directory whose spec setting comes from a nested config; only exclusions set in another config are locked. The walker already honoured such an exclusion. - Files:
src/tallyman/config.py,src/tallyman/cli.py,src/tallyman/walker.py,src/tallyman/tui/setup_app.py,tests/test_config.py,tests/test_cli.py,tests/test_walker.py,tests/test_setup_app.py - Plan:
plans/007-exclude-patterns/phase-01-merge-config-chain.plan.md - README,
CLAUDE.mdandgitignore.py's docstring describe worktrees, submodules and nested repositories. The README has a new "Git repositories inside your project" section listing the differences users meet, including the global excludes file and symlinked.gitignorefiles (tallyman reads them; git skips them). The docstring has the full list, including theGIT_DIRandGIT_COMMON_DIRenvironment variables. - Files:
README.md,CLAUDE.md,src/tallyman/gitignore.py,plans/006-tallyman-architect.plan.md - Plan:
plans/010-git-fidelity/phase-03-docs.plan.md
Fixed
- An image card legend too long for one row, such as five long names like
reStructuredText (specs), wraps onto more rows instead of running off the card's right edge, and the card grows to fit. A category line whose language list is too wide is shortened with an ellipsis, as the title already was. - The image card's bar is
BAR_HEIGHT(28) px tall, not 29, with 2 px gaps between segments, not 1, and it now ends flush with the content's right edge instead of 2 px short of the legend and footer. The separator line aboveTotal:no longer runs 1 px past that edge. Pillow's rectangle and line corners are inclusive, and the old coordinates weren't. - Files:
src/tallyman/image.py,tests/test_image.py,README.md,CLAUDE.md - Setup shows directory names exactly as they are on disk. A directory named
:rocket:no longer turns into an emoji, and a name ending in a backslash is no longer doubled, in the tree and on the root. The labels are built as RichText, never markup. - Files:
src/tallyman/tui/setup_app.py,tests/test_setup_app.py - Plan:
plans/011-setup-tui/phase-01-lazy-textual-and-text-labels.plan.md - Setup drops scan-root copies of nested config settings, as it already did for parent settings, and now does both when it opens as well as on save. Earlier versions wrote these copies, and they could never be removed in setup, so removing the nested setting later left the directory excluded. A copy is the same directory in the same list; a scan-root entry that overrides a parent's, or that a nested config overrides, stays. No message: the dropped settings still apply from their own files, so the counts don't change.
- A directory a config excludes is labelled as excluded by that config (
✗ name), even when gitignore or a pattern also matches it, matching the walker, which checks config exclusions first. A redundant scan-root exclusion there can now be toggled off, after which the label reads "(gitignored)" or "(excluded by pattern)". --setupno longer hangs when the scanned directory's.tally-config.tomlis a named pipe (FIFO) or another non-regular file: saving refuses it with the usual "could not save" warning and counts with the chosen settings for that run. A dangling symlink there is refused the same way, rather than creating its target wherever it points.- Files:
src/tallyman/config.py,src/tallyman/cli.py,src/tallyman/tui/setup_app.py,tests/conftest.py,tests/test_config.py,tests/test_setup_app.py,tests/test_cli.py,README.md,CLAUDE.md - Plan:
plans/011-setup-tui/phase-02-precedence-and-copies.plan.md - A project with no effective lines (only comments and blanks) no longer prints an empty bar and legend under the totals, and the image no longer leaves a blank band where the bar would be. The totals still say
0 lines. With--with-spacesthe bar is drawn as before. - A small "Other" always gets at least one cell of the terminal bar. Python 496, Rust 496 and Go 8 lines used to give Python and Rust 30 cells each and leave Other, still listed in the legend, with none. Bar widths are now split by largest remainder, in both outputs, and always fill the bar exactly.
--imageno longer crashes withValueError: x1 must be greater than or equal to x0when "Other" is under about 0.1% of the lines; every image segment is at least 4 px wide.- Files:
src/tallyman/aggregator.py,src/tallyman/languages.py,src/tallyman/display.py,src/tallyman/image.py,tests/test_aggregator.py,tests/test_display.py,tests/test_image.py,CLAUDE.md - Plan:
plans/008-output-hardening/phase-01-shared-report-model.plan.md - A named pipe (FIFO) with a source-file extension, or named
.gitignore, no longer hangs the scan. Only regular files are read: FIFOs, sockets and devices are skipped like symlinks, and a.gitignoreor.git/info/excludethat isn't a regular file, or a symlink to one, is treated as absent. - Files:
src/tallyman/walker.py,src/tallyman/gitignore.py,tests/conftest.py,tests/test_walker.py,tests/test_gitignore.py,README.md,CLAUDE.md - Plan:
plans/008-output-hardening/phase-02-files-stdout-config.plan.md - A terminal whose encoding can't show a character (Windows cp1252,
PYTHONIOENCODING=ascii) gets?instead of a crash, and the legend separator falls back to|with the rest of the ASCII output. An encoding with box drawing but no·(such as cp852) now gets the ASCII report too. - Files:
src/tallyman/cli.py,src/tallyman/display.py,tests/test_cli.py,tests/test_display.py,CLAUDE.md - Plan:
plans/008-output-hardening/phase-02-files-stdout-config.plan.md - A
.tally-config.tomlsaved with a byte-order mark loads instead of failing with "Invalid statement". A UTF-8, UTF-16 or UTF-32 mark picks the encoding, as it does for source files;--setuprewrites the file as UTF-8 without one. - Files:
src/tallyman/config.py,tests/test_config.py,tests/test_cli.py,README.md,CLAUDE.md - Plan:
plans/008-output-hardening/phase-02-files-stdout-config.plan.md - Spec and docs Markdown no longer merge into one block in the composition bar. Spec variants of docs languages now have their own colour (Markdown violet, reStructuredText dark sea green), in every terminal colour mode and on the image card.
- The bar's "Other" segment is now dark grey (
grey37). It no longer merges with TOML, or with Markdown on 16-colour terminals. - Files:
src/tallyman/languages.py,tests/test_languages.py,tests/test_display.py,tests/test_image.py,CLAUDE.md,plans/006-tallyman-architect.plan.md - Plan:
plans/009-spec-colours.plan.md .git/info/excludeis now honoured in git worktrees and submodules, where.gitis a file pointing at the real git directory. Worktrees use the main repository'sinfo/exclude, as git does. A.gitfile that is malformed, over 1 MB, or points at a directory with noHEAD(such asgitdir: ..) means noinfo/exclude, never an error; git refuses to run in such a checkout.- A gitignore or
[exclude]pattern ending in a lone backslash (a\) now matches nothing, as in git, instead of matching a name ending in\;!a\no longer re-includes anything. An escaped backslash before trailing spaces (a\\) no longer keeps the spaces. - Files:
src/tallyman/gitignore.py,tests/test_gitignore.py,CLAUDE.md,plans/006-tallyman-architect.plan.md - Plan:
plans/010-git-fidelity/phase-01-gitdir-and-escapes.plan.md - An empty
NO_COLORno longer disables colour; per no-color.org only a non-empty value does. - Solidity's colour changed from
bright_cyan, which SQL also uses, toslate_blue1, so the two are distinguishable in the bar. - README and
CLAUDE.mddescribe the current behaviour: config search and rebasing,[specs] ignore,".", no-terminal runs, what is never counted, and the language count (43, not 41). - New baseline tests for terminal rendering, CLI flags and colour switches, and checks that every language colour is valid and no new colour is shared.
- Files:
src/tallyman/cli.py,src/tallyman/languages.py,ruff.toml,pyproject.toml,README.md,CLAUDE.md,.cursor/rules/,tests/test_cli.py,tests/test_display.py,tests/test_languages.py - Plan:
plans/005-hardening/phase-05-hygiene-and-coverage.plan.md - Lines now end only at
\n,\r\nor a lone\r, and a final line without a newline still counts.str.splitlines()also split on form feeds, vertical tabs,\x1c-\x1e,\x85, U+2028 and U+2029, so files containing them were over-counted. A UTF-8 BOM no longer hides a comment on the first line, and UTF-16 and UTF-32 files with a BOM are counted instead of being skipped as binary. - Never counted, whatever
.gitignoresays, in any letter case: tallyman's own.tally-config.toml,package-lock.json,npm-shrinkwrap.json,pnpm-lock.yaml,*.min.jsand*.min.css. Symlinked files are skipped, so a link to a file in the tree no longer counts it twice. - Dockerfile variants are matched by name:
Dockerfile.dev,Dockerfile.go,Dockerfile-prodandDockerfile_ciare Docker, but a docs or data suffix (Dockerfile.md,Dockerfile.json,Dockerfile.toml, ...),.txtand.dockerignoreare not, andDockerfiles.pyis Python.docker-compose.override.yml,compose.prod.yamland other Compose variants are Docker, likedocker-compose.yml. - A directory name is printed literally in the report header:
[bold]is no longer read as Rich markup,:rocket:is no longer turned into an emoji, and control characters are replaced. In the image, a long directory name is shortened with an ellipsis to fit the card, and the file name is capped at 100 characters; a 253-character name used to crash with "File name too long". - The binary check reads only the first 8 KB instead of the whole file.
- Files:
src/tallyman/encoding.py,src/tallyman/counter.py,src/tallyman/walker.py,src/tallyman/languages.py,src/tallyman/display.py,src/tallyman/image.py,tests/test_counter.py,tests/test_walker.py,tests/test_languages.py,tests/test_display.py,tests/test_image.py - Plan:
plans/005-hardening/phase-04-counting-and-display-edges.plan.md - Setup TUI: the root directory can no longer be toggled;
xon it used to write""into the config and exclude every subdirectory. Children of an excluded directory now show as excluded, and toggling them does nothing, because they are never walked. Excluding then re-including a directory restores its spec status, and a child's own exclusion survives toggling its parent. Pressingstwice always restores what was there, andson the root marks or unmarks the whole tree, including when setup is run from insideplans/. Settings from nested.tally-config.tomlfiles are shown as locked instead of being copied into the root config, where they couldn't be changed anyway. Directory names containing[...]are shown literally. The TUI and the walker now share one rule for spec status (walker.spec_state()), with a test that checks they agree. - Files:
src/tallyman/tui/setup_app.py,src/tallyman/walker.py,tests/test_setup_app.py - Plan:
plans/005-hardening/phase-03-spec-overrides-and-tui.plan.md - A config found in a parent directory is now rebased onto the scanned directory. Running
tallyman proj/srcwithproj/.tally-config.tomlexcludingsrc/vendoranddocsused to countsrc/vendorand dropsrc/docs. A parent spec directory that covers the scanned directory now classifies its docs as specs. - A malformed or wrongly shaped root config is reported on stderr with exit status 1 instead of a traceback.
tallyman --setupreplaces a broken config instead of failing on it. - Directory names containing
",\or control characters are escaped when the config is saved, so the next run can read it. - Hand-edited config paths are normalised:
vendor/,./static,a//b,a/./b,a/../band/topnow match, and entries that climb out with..are dropped. Backslashes are treated as separators on Windows only. [specs] directoriesaccepts"."for the config's own directory, sotallyman --setuprun inside a spec directory no longer silently turns its specs into docs.- Scanning from inside an auto-detected spec directory (
tallyman plans/sub) now counts its docs as specs, the same as scanning from the repo root. - A config that isn't valid UTF-8, or is pathologically nested, is reported like any other bad config; in a nested directory it is skipped instead of aborting the scan.
- If setup can't save the config, tallyman warns and still counts with the chosen settings instead of crashing. A closed stdin no longer crashes, and a path typed in the wrong case on macOS (
proj/SRC) no longer defeats config matching. - Files:
src/tallyman/config.py,src/tallyman/cli.py,src/tallyman/walker.py,tests/test_config.py,tests/test_cli.py - Plan:
plans/005-hardening/phase-02-config-handling.plan.md - Gitignore handling rewritten to follow git's rules, in the walker and the setup TUI alike:
.gitignorefiles below the scanned directory are honoured, including the*file uv and virtualenv write inside a venv.- Each file's patterns are anchored to the directory it lives in, and a deeper file wins, so
!patterncan re-include. - Patterns judge one entry at a time, as git does:
!dist/re-includes thedistdirectory without re-including what a parent ignored inside it, and whitelist files (*,!*/,!*.py) keep the files they name. .git/info/excluderanks below.gitignore; nothing inside an ignored directory is counted, even when the scan starts inside it.- Odd lines behave as in git instead of breaking the scan: a BOM, an unreadable
.gitignore, a malformed bracket expression such as[z-a]or an unterminated[, POSIX classes such as[[:digit:]], and form feeds inside a pattern. - Verified against
git ls-files --exclude-standardon 1,900 generated repositories and about 75 hand-written edge cases, with no differences. - Known differences, all documented in
gitignore.py: matching is case-sensitive (git on macOS is not, by default);?matches one character where git matches one byte; and files git already tracks are still skipped if a pattern matches them.
- Relative paths are always built with
/, so config exclusions and spec directories nested two or more levels deep match on Windows. - Files:
src/tallyman/gitignore.py,src/tallyman/walker.py,src/tallyman/paths.py,src/tallyman/config.py,src/tallyman/cli.py,src/tallyman/tui/setup_app.py,pyproject.toml,uv.lock,tests/test_gitignore.py,tests/test_walker.py,tests/test_paths.py - Plan:
plans/005-hardening/phase-01-gitignore-correctness.plan.md