todo.tag-counts
How many todos carry each tag, for a tag sidebar: most used first, open todos only unless asked.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 10 tests, run in TypeScript, Python and Rust.
What it does
How many todos carry each tag, for a tag sidebar or filter chips: a list of `{ tag, count }`, most used first, ties in tag order (code point, via collections.sort-by, so `b-2`, `b10`, `b2`, not natural-number order).
- With `includeDone` false only open todos count, so the sidebar shows what is left to do; a tag whose todos are all done is left out rather than listed with a zero. - A todo counts once per tag, even if its tag list repeats one. - Tags are counted as stored. todo.item keeps them normalised (todo.normalise-tags), so `home` and `#Home` are already one tag.
For example
tagCounts(todos ×6, false)→ ×3 open todos only: ties go alphabeticallytagCounts(todos ×6, true)→ ×4 including done todos adds bills and more home and worktagCounts(, true)→ an empty list has no tags
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
export function tagCounts(todos: readonly Todo[], includeDone: boolean): readonly TagCount[]
| todos | Todo[] | |
| includeDone | bool | false counts only open todos |
| returns | TagCount[] | count descending, then tag ascending; no zero counts |
The type it declares, generated into your project
/** One tag and how many todos carry it. */
export interface TagCount {
readonly tag: string;
readonly count: number;
}
Your code names it in one line, in the file that uses it
import { tagCounts } from "#fune/todo.tag-counts@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { sortBy } from "./collections_sort_by.ts"; ← from collections.sort-by ^1.0.0 · built alongside by fune
import { type Todo } from "./todo_item.ts"; ← from todo.item ^1.0.0 · built alongside by fune
import { type TagCount } from "./todo_tag_counts_types.ts";
/**
* How many todos carry each tag, for a tag sidebar. A todo counts once per tag
* even if its list repeats one. With includeDone false only open todos count,
* so a tag whose todos are all done disappears. Most used first, then by tag
* in code-point order (collections.sort-by, so every language agrees).
*/
export function tagCounts(todos: readonly Todo[], includeDone: boolean): readonly TagCount[] {
const counts = new Map<string, number>();
for (const todo of todos) {
if (todo.done && !includeDone) continue;
for (const tag of new Set(todo.tags)) counts.set(tag, (counts.get(tag) ?? 0) + 1);
}
const rows = [...counts].map(([tag, count]) => ({ tag, count }));
const sorted = sortBy(sortBy(rows, "tag", "asc"), "count", "desc");
return sorted.map((r) => ({ tag: r.tag as string, count: r.count as number }));
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add todo.tag-counts
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./todo.tag-counts-1.0.0-typescript.fune, or fetch it from a terminal with fune pull todo.tag-counts@1.0.0:typescript.
The whole function, every language, is one file too: todo.tag-counts-1.0.0.fune, 13,164 bytes, sha256 3666265c41bbbb558c4084744eea9703df9e92cb87e8831c6fd0e5f1d8e56185. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before todo.tag-counts
after — your function gets the result and the arguments, and returns the final result.
// fune: after todo.tag-counts
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace collections.sort-by in todo.tag-counts
// fune: replace todo.item in todo.tag-counts
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show todo.tag-counts --steps.
// fune: step todo.tag-counts after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| open todos only: ties go alphabetically | todos ×6, false | → | ×3 |
| including done todos adds bills and more home and work | todos ×6, true | → | ×4 |
| an empty list has no tags | , true | → | |
| tags only on done todos are left out, not counted as zero | todos ×2, false | → | |
| todos without tags | todos ×2, true | → | |
| a tag repeated on one todo counts once | todos ×1, true | → | ×1 |
| ties are code-point order, not natural order: b-2, b10, b2 | todos ×1, false | → | ×4 |
| count beats alphabet | todos ×3, false | → | ×2 |
| one todo's tags come out alphabetically, not in its own order | todos ×1, false | → | ×2 |
| a done todo counts when asked | todos ×1, true | → | ×1 |
Files
| Path | Bytes |
|---|---|
| README.md | 601 |
| impl/python.py | 775 |
| impl/rust.rs | 1,632 |
| impl/typescript.ts | 1,000 |
| vectors.json | 6,278 |