Functional Weave
Code in TypeScript

todo.filter

The todos a list view shows: all, active or completed, optionally one tag, and a search box, in input order.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 17 tests, run in TypeScript, Python and Rust.

What it does

The todos a list shows for its three controls: the view tab (`all`, `active`, `completed`), the tag picked in the sidebar, and the search box. A todo must pass all three, and the result keeps the input order, so sort first (todo.sort) or after, whichever the app prefers.

- **View**: `active` is not done, `completed` is done, `all` is both. Any other view is an error: a typo should not quietly show an empty list. - **Tag**: `null` means any tag. Otherwise the tag is normalised exactly as stored tags are (todo.normalise-tags), so `"#Home"` and `" HOME "` find todos tagged `home`, and `"Home Office"` finds `home-office`. It is an exact tag match: `home` does not find `home-office`. A tag that normalises to nothing (`"#"`, `" "`) matches no todo, since a tag filter was asked for and no todo carries an empty tag. The todos' own tags are compared as stored; todo.item keeps them normalised. - **Search**: the query is trimmed of ASCII spaces, then matched as a case-insensitive (ASCII only) substring of the title, the notes, or the tags joined with spaces, using collections.search-text. So searching `bills` finds a todo tagged `bills`, and `home` finds one tagged `home-office`. An empty or all-space query matches everything.

For example

  • filterTodos(todos ×5, view all, tag —, query ) → ×5 all, no tag, empty search: everything in input order
  • filterTodos(todos ×5, view active, tag —, query ) → ×3 active hides done todos
  • filterTodos(todos ×5, view completed, tag —, query ) → ×2 completed shows only done todos

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 filterTodos(todos: readonly Todo[], filter: TodoFilter): readonly Todo[]
todosTodo[]in the order the list shows them
filterTodoFilterwhat the view tabs, tag sidebar and search box hold
returnsTodo[]the todos that pass all three, in input order

The types it declares, generated into your project

export type TodoView = "all" | "active" | "completed";

/** What a todo list is narrowed to. All three parts must match. */
export interface TodoFilter {
  readonly view: TodoView;
  /** as typed, e.g. "#Home"; null for any tag */
  readonly tag: string | null;
  /** search box text; empty matches everything */
  readonly query: string;
}

Your code names it in one line, in the file that uses it

import { filterTodos } from "#fune/todo.filter@^1";
impl/typescript.ts · 39 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { searchText } from "./collections_search_text.ts";  ← from collections.search-text ^1.0.0 · built alongside by fune
import { type TodoFilter } from "./todo_filter_types.ts";
import { type Todo } from "./todo_item.ts";  ← from todo.item ^1.0.0 · built alongside by fune
import { trimSpace } from "./todo_item_validate_draft.ts";
import { normaliseTags } from "./todo_normalise_tags.ts";  ← from todo.normalise-tags ^1.0.0 · built alongside by fune

const VIEWS = ["all", "active", "completed"];

/**
 * The todos a list shows for its view tab, tag and search box, in input order.
 *
 * The tag is normalised the way stored tags are (todo.normalise-tags), so
 * "#Home" finds "home". A tag that normalises to nothing ("#", "  ") matches no
 * todo: a tag filter was asked for, and no todo carries an empty tag. The
 * search is collections.search-text over the title, the notes and the tags
 * joined with spaces, so searching "home" also finds todos tagged home.
 */
export function filterTodos(todos: readonly Todo[], filter: TodoFilter): readonly Todo[] {
  if (!VIEWS.includes(filter.view)) {
    throw new RangeError(`unknown view "${filter.view}": expected all, active or completed`);
  }
  let wanted: string | null = null;
  if (filter.tag !== null && filter.tag !== undefined) {
    const tags = normaliseTags([filter.tag]);
    if (tags.length === 0) return [];
    wanted = tags[0];
  }
  const kept: Todo[] = [];
  for (const todo of todos) {
    if (filter.view === "active" && todo.done) continue;
    if (filter.view === "completed" && !todo.done) continue;
    if (wanted !== null && !todo.tags.includes(wanted)) continue;
    kept.push(todo);
  }
  const query = trimSpace(filter.query);
  if (query === "") return kept;
  const records = kept.map((todo, i) => ({ i, title: todo.title, notes: todo.notes, tags: todo.tags.join(" ") }));
  return searchText(records, ["title", "notes", "tags"], query).map((r) => kept[r.i 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 3 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.filter
Download for TypeScript todo.filter-1.0.0-typescript.fune · 35,437 bytes sha256 466e2ee25ec1505a24ba1cde2bf9cbd3f005941d6b23e5ee27eae9bff2cac2fd

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./todo.filter-1.0.0-typescript.fune, or fetch it from a terminal with fune pull todo.filter@1.0.0:typescript.

The whole function, every language, is one file too: todo.filter-1.0.0.fune, 39,877 bytes, sha256 80695baaa6bba264a8d5e8cd28af7869ab720455532ec7ce43c7d3725d6622e7. 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.filter

after — your function gets the result and the arguments, and returns the final result.

// fune: after todo.filter

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.search-text in todo.filter
// fune: replace todo.item in todo.filter
// fune: replace todo.normalise-tags in todo.filter

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.filter --steps.

// fune: step todo.filter 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.

CaseArgumentsExpected
all, no tag, empty search: everything in input order todos ×5, view all, tag —, query → ×5
active hides done todos todos ×5, view active, tag —, query → ×3
completed shows only done todos todos ×5, view completed, tag —, query → ×2
tag "#Home" finds todos tagged home, but not home-office todos ×5, view all, tag #Home, query → ×2
tag and view combine: open todos tagged home todos ×5, view active, tag Home, query → ×1
a tag typed with spaces is normalised to the stored slug todos ×5, view all, tag Home Office , query → ×1
a tag that normalises to nothing matches no todo todos ×5, view all, tag #, query →
the search is trimmed: " milk " finds the title and the notes todos ×5, view all, tag —, query milk → ×2
the search ignores ASCII case todos ×5, view all, tag —, query MILK → ×2
the search looks in tags too todos ×5, view all, tag —, query bills → ×1
Show the other 7 tests
CaseArgumentsExpected
a tag search is a substring: home also finds home-office todos ×5, view active, tag —, query home → ×2
the search looks in notes todos ×5, view all, tag —, query order → ×1
a search of only spaces matches everything todos ×5, view active, tag —, query → ×3
view, tag and search all have to match todos ×5, view active, tag home, query milk → ×1
nothing matches the search todos ×5, view all, tag —, query xyz →
an empty list stays empty , view active, tag home, query milk →
an unknown view is an error todos ×5, view archived, tag —, query → error: unknown view "archived": expected all, active or completed

Files

PathBytes
README.md1,275
impl/python.py1,646
impl/rust.rs2,576
impl/typescript.ts1,781
vectors.json26,078