Skip to content
ben-dev-auPublic

About

Local full-text search utility for documents on macOS: visual & intuitive UI, multi-format indexing, pdf structuring(!), BM25 ranking, an expressive query language, in-file navigation, inline and fullscreen reading views, and an "Open with…" launcher.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

FND

CI License: AGPL-3.0-or-later Python 3.13+ Platform: macOS | Linux (alpha) | Windows (alpha) Buy Me a Coffee

Fast, free, keyboard-driven document search for macOS. 100% local and private. Built for Markdown, and indexes PDF, DOCX, PPTX, EPUB and 35 more file types, with strong BM25 ranking, in-file navigation, an "Open with…" launcher, and a lazygit-style TUI.

fnd: results, outline and preview

macOS is stable; Linux and Windows are alpha: CI-tested, never used in anger. Open an issue when you hit something. See Platform support.

Requirements

  • macOS 15+ on Apple silicon: supported.
  • Linux or Windows: alpha. See Platform support.
  • Homebrew and uv bring Python 3.13 with them; pipx needs it installed.
  • A modern terminal (see Terminal compatibility).

Install

Pick one of the options below. They install the same tool, so you only need one.

Option 1: Homebrew (macOS)

brew install ben-dev-au/tap/fnd

Homebrew is the standard macOS package manager. Install it once, then run the line above. It installs a prebuilt binary.

Option 2: uv or pipx (macOS, or Linux / Windows in alpha)

uv tool install fndr        # or:  pipx install fndr

To run it once without installing: uvx --from fndr fnd.


Either way, launch the app by typing fnd.

On PyPI the package is named fndr (fnd was taken); the command stays fnd.

Releases carry build provenance; see SECURITY.md to verify a download.

Features

  • Markdown first, 40 file types: Markdown and plain text, PDF, DOCX, PPTX, OpenDocument, EPUB, HTML, 22 code languages, data files (JSON, YAML, CSV, …), Jupyter notebooks.
  • Named collections: group sources and search them individually or together; each source has its own Index filters and honours .gitignore / .fndignore.
  • Strong ranking: BM25 with regime-aware fusion (strong-signal / fusion / cascade) for stable results across corpora of different sizes.
  • Auto-fuzzy matching: a word no file holds is respelt, and the line under the query says so; Ctrl+T searches as typed.
  • Expressive query language: phrases, boolean, proximity, fuzzy, field qualifiers, wildcards, regex, date filters, and Markdown frontmatter filters (see Search how-to).
  • lazygit-style TUI: live search as you type, syntax-highlighted preview, an Outline pane, match navigation, Reading View, and in-file navigation that jumps to the matching PDF page, PPTX slide, or Markdown heading.
  • "Open with…" launcher: open a hit in Preview, Skim, Obsidian, VS Code, PDF Expert, or your own configured app, with page/line/heading deep-links where the app supports them (see Open with…).
  • Obsidian integration: vault auto-detection, frontmatter filters, and line-precise jumps via the Advanced URI plugin.
  • Semantic search (opt-in): a local embedding model blended into the BM25 ranking (see Semantic search).
  • PDF texturising (opt-in): structured PDF extraction (headings, lists, tables, bold/italic) with a shared PDF Texture Cache (see PDF texturising).
  • 100% local and private: no account, no cloud, no telemetry. The network is used only to download an opt-in extra.

Platform support

Read the table as what is implemented, not what is verified:

Capability macOS Linux (alpha) Windows (alpha)
Maturity tested in use CI only CI only
Search · indexing · TUI · preview ✓ ✓ ✓
Open in app / Open with… ✓ ✓ ✓
Reveal in file manager (R) Finder file-manager --select → folder Explorer /select
PDF page-jump on Open Skim, Preview Zathura, Okular SumatraPDF
PDF texturising (opt-in) ✓ ✓ ✓
Created-date filter ✓ (birth time) best-effort (statx) ✓ (creation time)
Cloud-only file handling iCloud Drive not detectable OneDrive & co.
Finder tag filtering ✓ no OS equivalent no OS equivalent
YAML tags: filtering ✓ ✓ ✓
  • Where an OS has no equivalent, the feature is skipped, not an error.
  • "Open with…" lists only the apps you have installed.
  • Linux and Windows app handlers follow each app's documentation and are unconfirmed; to add a viewer, see docs/apps.md.
  • Reports from Linux and Windows are valuable, "it worked fine" included.

Quick start

First collection: from the TUI

  1. Run fnd. With no collections yet, it opens straight into Add collection.
  2. Fill in the Name and the source's Path (~/… is fine). Optionally set its Index filters (file types, excluded paths and folders, tags, size, dates) and Frontmatter rule.
  3. Ctrl+S saves and opens the collection's page; Update index builds it.
  4. Esc back to the search and start typing.
  • Later collections: Tab out of the query bar, press :, then Collections → Add collection.
  • Add more folders: open the collection → Sources → Add source.
  • Saving never indexes: ↻ marks a collection that is behind; press u on it.

First collection: from the CLI

fnd collection add notes --source ~/Notes --include "**/*.md" --exclude-dir archive
fnd collection reindex notes

Everyday use

fnd binary tree -c notes     # open the TUI on that query, scoped to notes
fnd binary tree              # the same, across your selected collections

Obsidian

  1. Add the vault as a collection, as above (Path: the vault folder).
  2. Open notes in Obsidian: Settings (:, outside the query bar) → Collections → the vault → Sources → the source → App: Obsidian (the Obsidian vault name fills in) → Ctrl+S.
  3. Install Obsidian's Advanced URI plugin to land on the matching line; without it, ⌥O opens the note at the matching heading.
  • Frontmatter: filter a query with [Status == 'draft'], or a source with a Frontmatter rule (--filter on the CLI). See Markdown frontmatter filter.
  • tags: work with the Filters pane and --tag; tag a note no_index to keep it out.
  • .obsidian/ is hidden, so never indexed.

Using the TUI

Run fnd with no arguments:

  • Query bar (top): results update as you type; same query language as the CLI.
  • Results (left): hits grouped by file, with the Outline, Collections and Filters panes beneath.
  • Preview (right): the matching passage, search terms highlighted.

Moving around with the keyboard

Key What it does
↑ / ↓ Move the cursor up/down through results (vim's k / j also work).
⌥↑ / ⌥↓ Skip: hold Option (Alt) to move through results without loading each preview. The preview loads again on a plain ↑/↓ or Enter.
Enter Load the highlighted result into the preview (handy right after an Option-skip).
→ Expand the focused file to its matching sections; press again to drill into the first.
← Collapse the focused node, or back out to its parent (lazygit-style).
Ctrl→ / ⌥→ Expand all: expand the focused node and its whole subtree (results, collections and filters trees).
Ctrl← / ⌥← Collapse children: fold away every descendant, keeping the node itself open.
Tab Move focus to the next pane.
/ Jump back to the query bar to refine your search.
r p c f Focus Results, Preview, Collections, Filters.
o Focus the Outline. Enter on a heading (or → on one with nothing to expand) moves the preview there.
n / b Next / previous match in the preview.
↑ / ↓ When the preview pane is focused, scroll the preview.
z Reading View: hide the sidebar so the preview fills the width, for clean text selection.

The outline

  • Lists the headings of the previewed document: document headings, slide titles, a PDF's bookmarks (or, without any, its preview headings).
  • Follows you as you move between results and scroll.
  • Code, data and plain-text files have no outline.

Option-skip on Apple Terminal: for ⌥↑ / ⌥↓ to reach fnd, enable Settings → Profiles → Keys → Left Option key → Esc+. iTerm2 and most modern terminals work without any change.

⌥O needs Option to send Meta (Alt). Without that terminal setting, Option+O types a character instead (ø on a US layout). Ctrl+O works in every terminal on every keyboard layout, so use it wherever you need Option for typing: Danish, Norwegian and many other layouts put letters and symbols there.

Expand/collapse all: on macOS use ⌥←/⌥→, since Mission Control usually takes ⌃←/⌃→. On Windows and Linux, Ctrl+arrow works.

Opening and acting on a result

Key What it does
⌥O / Ctrl+O Open the file at the matching page / slide / line / heading (Alt+O on Windows and Linux).
O Open with…: a picker of every app that handles this file type. Use ↑↓ then Enter, or press the letter shown next to an app; Esc cancels.
R Reveal: show the file in your file manager (Finder on macOS, File Explorer on Windows) with it selected, without opening it. Also on the last row of the O picker.
: Open the Settings & Commands menu: every setting and action in one searchable, full-screen list. Not from the query bar, where it types a :.
? Keybindings cheat sheet (press again to dismiss).
Ctrl+F Toggle auto-fuzzy matching (persists to your config).
Shift+Esc / Ctrl+T Search the current query as typed, respelling no word; press again to respell. Shift+Esc needs the Kitty keyboard protocol (Kitty, Ghostty); Ctrl+T works everywhere.
h Toggle search-term highlighting in the preview.
w Warm the focused file completely, so scrolling anywhere in it is instant. Asks first on a large file; press again on that file to stop.
X Clear every active filter (file type, dates, tags); the collection scope stays.
Ctrl+Enter / ⌥Enter In Collections or Filters, toggle a row without re-running the query, to change several at once.
u In Collections, update the highlighted collection's index (↻ marks one that needs it).
Ctrl+/ In Collections or Filters, search the pane: type to show only matching rows, Esc clears. ↑ from the top row gets there too.
Ctrl+Y Copy the query and active filters as a runnable fnd command.
q / Ctrl+C Quit. Esc backs out of any overlay or nested screen.
  • Settings menu (:): ↑↓ or j/k to move, Enter to open / edit / toggle, / to filter rows, Esc or ← to step back.
  • Rebind keys in keybindings.toml: Settings → External → Keybindings file.

Terminal compatibility

  • macOS: iTerm2, Ghostty, Kitty or WezTerm. Terminal.app works, but how it draws borders depends on the font (Menlo is a reasonable default).
  • Linux: Kitty, WezTerm, Alacritty, GNOME Terminal or Konsole (expected to work, not yet confirmed).
  • Windows: Windows Terminal (built into Windows 11) or WezTerm. The old console draws borders and colours poorly.
  • If the TUI looks wrong, try another terminal first.

Command reference

Command What it does
fnd <query> -c <collection> The usual way in: launch the TUI with <query> pre-filled, scoped to a collection.
fnd <query> Launch the TUI with <query> pre-filled, across the remembered collections.
fnd / fnd -c <collection> Launch the TUI with an empty query (-c all for every collection).
fnd tui [query] Explicitly launch the TUI; use it when the query starts with a subcommand name (fnd tui version). Takes the filters below.
fnd search "<query>" Terminal search. Flags: --limit, -c/--collection, --meta, --as-typed, --explain N, plus the filters below.
fnd index <root> Ad-hoc index one folder into the default collection (--no-embed skips semantic embedding).
fnd collection list List configured collections and their sources.
fnd collection add <name> Add (or extend) a collection in the config TOML.
fnd collection reindex <name> Index or re-index a configured collection (--rebuild to start fresh).
fnd config show Print the effective merged config as JSON.
fnd config path Print the path to the config TOML.
fnd config edit Open the config TOML in $EDITOR (creates a template if missing).
fnd config validate Validate the config TOML.
fnd extras list List optional extras and their installed status.
fnd extras status Show installed extras with disk usage.
fnd extras install <name> Install an extra after a disk-impact disclosure prompt.
fnd extras uninstall <name> Remove an extra (indexed chunks remain).
fnd cache status / info / prune / prune-orphans / clear Manage the PDF Texture Cache.
fnd version Print the fnd version.

Search filters

fnd search and fnd tui take the same filters as the TUI's Filters pane:

Flag Meaning
--tag <name> Only files carrying this tag. Repeatable.
--not-tag <name> Exclude files carrying this tag. Repeatable.
--tag-match all|any Combine multiple --tags. Default all.
--created <window> Created within today/yesterday/week/month/year.
--modified <window> Modified within the same windows.
--kind <kind> A file type or category (md, pdf, code, ebooks). Repeatable.
fnd search "risotto" --tag recipe --not-tag draft --created month
  • Tags come from YAML frontmatter (tags:, Obsidian-style) and, on macOS, Finder tags.
  • --tag project also matches project/alpha; case and a leading # don't matter.
  • defaults.tag_sources picks which tags are read; no reindex needed.
  • Created dates are the filesystem's creation time: macOS birth time, Windows creation time, statx-capable Linux filesystems (e.g. ext4). A file without one matches only the unfiltered window.

Open with… apps

  • ⌥O or Ctrl+O opens a hit in its resolved app at the matching page, slide, line or heading, where the app allows.
  • O opens the Open with… picker, listing only installed apps: Skim, Preview, PDF Expert (macOS), Zathura, Okular (Linux), SumatraPDF (Windows), plus Obsidian, VS Code, System Default.
  • Set a default app per file type with [app_defaults], or per source.
  • Add your own app: docs/apps.md.

Collections & sources

  • A collection is a named group of folders (sources) you search together; each source's Index filters decide which of its files get indexed.
  • fnd searches all collections by default; tick some in the Collections pane to narrow it, and the choice is remembered.
  • fnd -c <name> narrows one launch; -c all widens it, leaving the remembered choice alone.

Searching several collections marks each result with its collection:

  • Extension in the collection's colour; a shape too, past five collections.
  • Sidebar is the key; the preview's bottom border names it.
  • A file in more than one: unmarked.

Globs match the path relative to the source root. *, ? and [abc] stop at a /; a whole ** segment spans zero or more folders, so **/*.md catches both a.md and notes/deep/a.md. The ~~ operator uses the same language.

Manage collections from the TUI, the command line or the config file; all three edit the same file.

From the TUI

Tab out of the query bar, press : to open Settings, move to Collections, then:

  • Add a collection: choose Add collection and fill in:

    • Collection: its Name.
    • Source, its first folder: a Path (~/… is fine), Follow symlinks, then what gets indexed from it: Index filters (file types, excluded paths, tags, size, dates), the Frontmatter rule, and Restrict to these paths.

    Press Ctrl+S to save; you land on the new collection's page, where Update index builds it.

  • Add a source to an existing collection: open the collection, then Sources → Add source, and set the path, its Index filters, an optional per-source app, and (for Obsidian) the vault name. Ctrl+S saves and returns; Ctrl+A saves and adds another.

  • How editing works:

    • A single setting (a toggle, a picker, a value) applies the moment it changes.
    • A form (a source, Add collection, Index filters) saves with Ctrl+S; Esc goes back, and asks first if there are unsaved changes.
    • A screen opened from a form (a source's filters, a picker) carries its changes back to the form on Esc.
    • Saving never indexes. A collection whose index no longer matches its config shows ↻ in the Collections pane; press u there, or on its Settings page, to update it.

From the command line

# One source per command; run it again to add another folder to the collection
fnd collection add reading --source ~/Documents/Books
fnd collection add reading --source ~/Documents/Papers

# Index filters: globs, excluded folders, a frontmatter rule
fnd collection add notes --source ~/Notes --include "**/*.md" --exclude "drafts/**"
fnd collection add work --source ~/Work --exclude-dir archive --exclude-dir projects/old
fnd collection add vault --source ~/Vault --filter "NOT ('private' in tags)"

fnd collection list             # show what's configured
fnd collection reindex reading  # build/update the index (--rebuild to start fresh)

From the config file

Run fnd config edit to open the TOML in $EDITOR, then fnd config validate to check it.

fnd generates the file: every key is listed with what it does, and a commented-out line is its default, so uncommenting one changes it. Hand-edit freely; your values are kept, but layout and comments are regenerated whenever fnd writes the file.

Put anything you want kept between the notes markers:

# >>> notes: kept verbatim when fnd rewrites this file
# Vault re-indexed 2026-09-01.
# parked: exclude_tags = ["no_index", "draft"]
# <<< notes

config_version at the top records the format. fnd upgrades an older file on first launch and leaves a config.toml.bak-<timestamp> beside it. A file from a newer fnd is refused rather than silently misread.

Keeping files out of the index

Index-time filters decide what gets indexed; the Filters pane narrows what an existing index returns. They live in [defaults.filters], and per source in […sources.filters] which overrides field by field. Settings → Filters → Index filters edits the defaults; a source's Index filters row edits it.

Filter What it does
(always on) Hidden files and folders (.foo) are skipped, whatever the filters say. Only an includes glob naming a dot-prefixed component (.obsidian/**) admits one, and then only the paths that glob itself matches. A file whose own name starts with . still meets the default **/.* exclude: to index one, give the source its own excludes without that glob.
respect_gitignore Honours every .gitignore down the tree, with git's rules: negation, directory patterns, nearest file wins. Not .git/info/exclude and not core.excludesFile: neither is in the tree, so honouring them would make one corpus index differently on two machines. Use .fndignore for a rule that is yours alone. On by default.
respect_fndignore The same syntax in a .fndignore, read only by fnd: how to hide something from search without hiding it from git. On by default.
excludes Path globs never indexed; a folder a name/** glob covers is pruned unopened. Settings shows them as Excluded paths (presets such as Node modules) plus Excluded globs you type. Set once in the defaults for every source; a source's own list replaces it. System files by default: **/.*, .DS_Store, Thumbs.db, desktop.ini and .git.
exclude_dirs Folders under one source's root that are never indexed, e.g. ["archive", "notes/drafts"]; pruned unopened, matched exactly (case-sensitive), and an includes glob cannot bring them back. Settings shows them as Excluded folders (a tree to tick, where a ticked folder covers everything in it) plus Excluded folders (as paths) you type. Per source only; the source still inherits excludes. fnd collection add --exclude-dir sets it.
include_tags Index only files carrying one of these tags; the tag rows' ●. Empty means the tag is not consulted.
exclude_tags Tags that keep a file out; the tag rows' ⊘. From any source fnd reads: macOS Finder tags and a note's YAML tags:. Defaults to ["no_index"], so tagging a file no_index either way keeps it out.
kinds Restrict to given file types. Empty means every supported type. A source's includes folds into this when it names every suffix of a type (["**/*.md", "**/*.markdown"], not ["**/*.md"] alone). Anything else stays a glob.
min_size / max_size Bytes. Keeps stubs, and multi-hundred-megabyte scans, out.
created_after / created_before ISO dates (2024-01-01). A fixed bound, not the Filters pane's rolling window: a window would change what the index holds as time passed. A file with no creation date (best-effort on Linux) is kept.
modified_after / modified_before ISO dates, same semantics.
frontmatter A frontmatter predicate, same syntax as a query: type == 'note' AND status != 'draft'. Applies to the kinds that carry frontmatter: .md and its variants, and .txt. A note without a block, or with one that fails the rule, is kept out; a PDF is out of scope and passes. A block that fails to parse fails the rule.
expression A predicate over any file, using file.kind, file.size, file.modified, file.tags.os, file.path and the like. The rows above are written in terms of it.

The tree shows one branch per rule. Markers: ● keep only these, ⊘ never these, ○ no rule. An empty branch says what that means, so the file types read (every type). ⊘ is reachable on the tag rows; file types are an include-only list, so excluding one is a typed rule (t) and that branch says so.

Beneath the tree the set is shown as the expression it compiles to; t edits it. Rows and text are two views of one filter and update each other. Anything the rows cannot express stays in expression. A per-source row set to - overrides to nothing; left empty it inherits.

No rebuild needed: the next update prunes what a new filter excludes.

Ignore files apply from the source folder downwards, so an enclosing repo does not govern it. A .gitignore often excludes large PDFs precisely because they are big; set respect_gitignore = false if you want those found.

Configuration

The config lives in your platform's app-data directory (run fnd config path to see the exact location): ~/Library/Application Support/fnd/ (macOS), ~/.local/share/fnd/ (Linux), or %LOCALAPPDATA%\fnd\ (Windows). fnd also reads ~/.config/fnd/config.toml on any OS if you keep it there. fnd config show prints the effective merged config; fnd config validate checks it before you rely on it.

Each collection is one or more [[collections.<name>.sources]] tables. The file fnd writes documents every key inline; this is the shape, with the generated comments stripped:

config_version = 1

[defaults]
collection    = "all"      # scope a fresh profile starts with: "all" or a name
result_limit  = 200        # max results per query
fuzzy_enabled = true       # auto-fuzzy in the cascade fallback (toggle with Ctrl+F)

# A collection named "papers" with two source folders.
[[collections.papers.sources]]
path     = "~/Documents/Research"
includes = ["**/*.md", "**/*.pdf"]        # ORed globs; see filters.kinds below
follow_symlinks = false

[collections.papers.sources.filters]
excludes = ["**/.git/**", "archive/**"]   # replaces [defaults.filters] excludes
exclude_dirs = ["drafts/old"]             # folders under this source; keeps inherited excludes

[[collections.papers.sources]]
path = "~/Notes"

[collections.papers.sources.filters]
kinds       = ["md"]
frontmatter = "Status == 'published' AND NOT ('private' in tags)"  # notes only

# Index-time filters every source inherits. A source's own [filters] table
# overrides these field by field; anything left out is inherited.
[defaults.filters]
respect_gitignore = true            # honour .gitignore, with git's own rules
respect_fndignore = true            # same syntax, read only by fnd
excludes          = ["**/node_modules/**"]  # replaces the System files default
exclude_tags      = ["no_index"]    # Finder tags and YAML tags:, see below
# include_tags    = ["reference"]   # index only files carrying one of these
# kinds           = ["md", "pdf"]   # omit to index every supported type
# max_size        = 50_000_000
# modified_after  = 2020-01-01
# expression      = "file.size < 50_000_000"

# Default app per file type for the Open shortcut (⌥O or Ctrl+O). Built-in ids:
# system, obsidian, vscode (all OSes); skim, preview, pdf_expert (macOS);
# zathura, okular (Linux); sumatra (Windows).
[app_defaults]
pdf = "skim"
md  = "obsidian"

# Define your own app (ready-made blocks live in docs/apps.md).
[apps.marked]
display_name = "Marked 2"
handles      = ["md"]
argv         = ["open", "-a", "Marked 2", "{path}"]

[defaults] also controls preview behaviour and auto-resume. Every option is documented in the file itself: run fnd config edit to read it. After changing collections or sources, run fnd collection reindex <name>, or Update index on the collection's Settings page.

Semantic search (opt-in)

  • Plain-word searches also rank by meaning. Quoted, {N}, wildcard, - and regex searches stay word matches.
  • ≈: a file only the model found. Other rows keep fnd's score.
  • Model: granite-embedding-small-english-r2, int8, 52 MB, Apache 2.0, English. Runs locally.
  • Install: Settings → Preferences → Semantic search, or fnd extras install semantic (~160 MB).
  • Embeds in the background after indexing, ~50 sections/s (M1 Max). Resumes after quitting.
  • Config: semantic_search, show_embedding_progress; per collection, semantic.
  • --no-embed on fnd index and fnd collection reindex skips embedding.
  • fnd extras uninstall semantic removes the model, vectors and packages.
  • A failed or cancelled install removes what it added.
  • Windows, uv tool install: quit every open fnd, then run the uv tool install command it prints.

Indexing

PDF texturising (opt-in)

PDFs render as flat extracted text by default. The opt-in pdf-structure extra (the texturising engine) adds headings, lists, tables, bold/italic, and recovered image-rendered tables. It installs via uv.

  • TUI: Settings → Indexing & PDF Texture → Install texturising engine. It states the disk impact (~900 MB) before any download; Esc sends the install to the background, c cancels.

  • CLI:

    fnd extras install pdf-structure   # ~900 MB total, with disclosure prompt
    fnd extras status                  # disk usage per installed extra
    fnd extras uninstall pdf-structure # indexed chunks remain until the next reindex
  • Then run Texturise flat PDFs on each collection's Settings page (or fnd collection reindex <name> --rebuild); new PDFs are texturised as they are indexed.

  • Packages: pymupdf4llm (pulls pymupdf-layout, Polyform Noncommercial 1.0) and docling-slim[standard] (Apache-2.0). fnd redistributes neither, so Polyform's non-commercial restriction binds your use directly: check it before commercial use. ML weights (~400 MB) download on first use.

  • Cost: ~30 s per PDF on an M1 Max, so 200 books take about 2 hours, once. Later updates re-process only changed files.

PDF Texture Cache

  • Content-addressed in ~/Library/Caches/fnd/pdf-structure/ on macOS, the platform cache dir elsewhere (fnd cache info prints it).
  • Shared across collections: a file in two collections is texturised once.
  • TUI: Settings → Indexing & PDF Texture: Saved texturings, Remove orphaned texturings, Clear texture cache (asks first; cannot be undone).
  • CLI: fnd cache status / info / prune / prune-orphans / clear.

Auto-resume on launch

  • Quitting, Ctrl+C, sleep or a closed terminal mid-index leaves a state file at reindex/<collection>.state.toml in fnd's data directory.
  • The next launch resumes in the background; cached files return in milliseconds, so it picks up where it stopped.
  • Turn off with Settings → Indexing & PDF Texture → Auto-resume on launch, or defaults.indexer_auto_resume = false.

Search how-to

fnd's query bar accepts plain words, phrases, boolean expressions, fuzzy and proximity matches, wildcards and regex, field qualifiers, date filters, and markdown frontmatter filters. They compose freely.

The basics

You type What it does
invoice One term. Searches body, title, headings, and filename. Stemmed (invoice = invoices).
holiday budget plan Several terms, ranked. Docs matching more terms rank higher; all-term docs reach the top.
holiday AND budget Require both terms.
"follow up email" Exact phrase, in order. Also matches follow-up email.
holiday OR vacation Either term. A note holding both ranks no higher than one holding either.
holiday|vacation Same as (holiday OR vacation).
budget NOT tax Has budget, excludes tax.
+recipe -dessert + require, - exclude (shorthand for AND / NOT).
recipe -(dessert OR cake) A sign applies to a whole group: excludes both.
(flight OR train) AND booking Group with parentheses, to any depth.

Phrases

Quoting is the biggest precision win. Quote any common phrase:

You type Matches
return on investment The words anywhere in a chunk. Noisy.
"return on investment" The three words together, in order. Also matches return-on-investment.

Proximity

Find terms near each other, in any order. {N} and NEAR/N are equivalent:

You type Means
{5} project deadline The two terms within 5 tokens of each other.
project NEAR/5 deadline Same.
{20} cancel the gym membership All four words within ~one line of text.
{20} (cancel OR end) membership Either word within 20 tokens of membership.
{60} lost luggage claim Within ~a few lines.
{500} kitchen renovation quote Within ~one page.
  • Scale: 5 ≈ very near, 20 ≈ a line, 60 ≈ a few lines, 500 ≈ a page.
  • Order doesn't matter. Quote ("project deadline") when it does.
  • {N} covers the words and (a OR b) groups right after it, up to the first operator, other (, or filter, so {10} lost luggage kind:pdf slops only lost luggage.
  • Can't cross a chunk boundary; if terms are far apart, drop the {N}.
  • NEAR/N takes exactly two words; either may be an (a OR b) group.
  • A word may carry * or ~N ({5} insurance~ claim).

Fuzzy matching for typos and variants

Suffix ~1 or ~2 to allow that many edits per term. An adjacent transposition (ie ↔ ei) counts as one edit:

You type Matches
recieve~1 receive (one swap).
accomodation~2 accommodation and near spellings.

Works on a single term or alongside others (parcel recieve~1). Use sparingly on short terms: cat~2 matches almost everything. A typed ~N may change the first letter (kalendar~1 finds calendar); automatic fuzzy keeps it, and a plain misspelling is respelt instead.

  • A word no file holds is respelt, and the line under the query names it: "Also searched restuarant as restaurant."
  • Shift+Esc or Ctrl+T searches as typed instead (fnd search --as-typed); a quoted word is never respelt.

Field qualifiers

A field qualifier is a hard filter: it narrows the result set (it does not just boost), so you can combine it with search terms to constrain them.

You type What it does
title:invoice Only documents whose title contains invoice.
heading_path:"chapter 4" Only sections under that heading path.
author:austen Only documents with that author metadata.
kind:pdf Only a file type or category (md, pdf, code).
path_tokens:taxes Only paths containing taxes.
title:(recipe OR menu) Group alternatives within one field.
has:author Only documents that have a non-empty author field.

Combine with terms to constrain them: kind:pdf "travel insurance" finds the phrase in PDFs only.

Collections

fnd organises sources into named collections. The shorthand c: scopes a search to one or more:

You type What it does
c:work deadline Search the work collection only.
c:work,home budget Search two collections.

Without c:, the Collections pane selection applies.

Page, slide, and date filters

Numeric ranges use [low TO high]. Shorthand for one-sided comparisons:

You type What it does
page:5 Exact page 5.
page:>20 Page 21 onward.
page:[10 TO 20] Pages 10 to 20 inclusive.
slide:<5 First four slides.
mtime:today Modified today.
mtime:week / mtime:month / mtime:year Within the last 7 / 30 / 365 days.
mtime:>2024-01-01 Modified on or after 2024-01-01.
mtime:[2024-01-01 TO 2024-06-30] Modified in that ISO range.

created: takes the same forms as mtime:.

Wildcards and regex

You type Matches
garden* Words starting with garden.
*ship Words ending in ship.
photo*aphy photography.
gr?y ? = exactly one character: gray, grey.
invoice due? A ? ending a word is a question mark: due.
/colou?r/ A regular expression over whole words: color, colour.

Wildcards and regexes match words as written (accents folded, any case), not their stems.

You rarely need *: search already matches word variants (invoice finds invoices). Wildcards, fuzzy, regex, and phrases all work inside AND / OR / NOT / ().

Markdown frontmatter filter

Filter notes by their YAML frontmatter with a […] predicate. The same expression is a source's filters.frontmatter, editable from its Frontmatter rule row. String values use single quotes; double quotes mark a field name with spaces ("Due Date"):

You type What it does
deadline [Project == 'Website'] Notes where the Project field equals that value.
[Type == 'Meeting' OR Type == 'Idea'] Either value.
[Type in ['Meeting', 'Idea']] Any value in the list; a list field matches if it holds one.
[Type == 'Reading List'] Also matches a list holding it, as Obsidian writes a list property.
budget [Project == 'Garden' AND Year >= 2024] Compound predicate.
['urgent' in tags] urgent is an element of the tags list.
[NOT ('private' in tags)] Exclude a tag, keeping notes that have no tags:.
[Project ~~ 'Home *'] Glob a string value (not the body-search ~N fuzzy).
["Due Date" < 2026-01-01] A field name with a space, double-quoted.

Operators: == != < <= > >= ~~ (glob, string fields), in / not in (list membership), AND, OR, NOT, parentheses. Values are single-quoted strings, numbers, ISO dates, or true/false/null. Numbers may use _ as a digit separator, as in TOML (50_000_000). Inside a quoted string, \' is a literal quote and \\ a literal backslash; a backslash before anything else stands for itself, so a path like 'C:\temp' needs no escaping. Only notes can match (markdown and plain text); other kinds are left out. A rule on its own, with no search words, lists every note it admits.

A missing field fails every comparison, including negative ones. On a note with no tags:, ['x' not in tags] is false, so the note is dropped; [NOT ('x' in tags)] is true, so it is kept. To exclude a tag without also losing untagged notes, negate the whole test: NOT (… in …). The same rule makes [Status != 'draft'] skip notes that have no Status at all.

in needs a real YAML list. If some notes write tags: private as a bare string, add AND NOT (tags ~~ '*private*') to catch them too.

Composing: worked examples

"birthday party"                        # exact phrase
{10} lost luggage claim kind:pdf        # three terms within 10 tokens, PDFs only
c:work deadline [Project == 'Website']  # term + collection scope + frontmatter filter
title:"user guide" heading_path:setup   # constrain to one guide's setup section
kind:pptx slide:>10 roadmap             # later slides mentioning the roadmap
mtime:month garden*                     # recently modified docs mentioning garden-anything
garden* AND budget                      # a wildcard required inside a boolean
(flight OR train) AND tiket~1           # grouping with a fuzzy term
"out of office" OR holiday              # an exact phrase OR a loose term

A few common pitfalls

  • Quoting a single word does nothing useful. "invoice" is the same as invoice. Quotes only help for multi-word phrases.
  • OR and AND are case-sensitive. Lowercase or / and are treated as ordinary terms. Always uppercase boolean operators.
  • Standalone stopwords are dropped. the invoice searches just invoice; common words (the, in, of, …) are removed from unquoted queries. To match a phrase that includes them, quote it: "return on investment".
  • Proximity is per-chunk. A phrase or {N} query can't span a chunk boundary. If the terms are paragraphs apart, drop to a loose multi-term query.

Contributing

Bug reports and focused PRs are welcome; see CONTRIBUTING.md for dev setup and the "Open with…" app-catalogue workflow.

Security

fnd runs locally with no telemetry; it uses the network only to download an extra you install. For the threat model and private vulnerability reporting, see SECURITY.md.

Support

fnd is free and always will be. If it's earned a spot in your workflow and you feel like buying a broke student dev a coffee, the button's there. Much gratitude if you do, but I hope you find the tool useful either way.

Buy Me a Coffee

License

GNU AGPL-3.0-or-later © 2026 Ben Davidson

Use it, read it, modify it, run it, privately, for any purpose, with no obligations at all. If you distribute it, or run a modified version as a network service, that version has to ship its source under this same licence. Put plainly: your use is either private, or it is open source.

fnd links PyMuPDF (AGPL-3.0-or-commercial), so any distributed combination already carried AGPL terms; this licence states what was true of fnd all along.

Releases up to and including fndr 0.0.5 were published under MIT and remain MIT.

About

Local full-text search utility for documents on macOS: visual & intuitive UI, multi-format indexing, pdf structuring(!), BM25 ranking, an expressive query language, in-file navigation, inline and fullscreen reading views, and an "Open with…" launcher.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages