Skip to content

Repository files navigation

Acervus

A terminal filesystem tagging tool (in the spirit of TMSU). Acervus organizes files across your disk with marks (labels) and stacks (named groups), keeping the index in a local SQLite database while your files stay exactly where they are.

📖 User's manual — acervus.fancysnake.dev (source in docs/)

Status: MVP complete. Acervus scans your configured roots, browses the indexed files, and marks and stacks them from the TUI, with filtering on each. FEATURE_PLAN.md records how it was built.

Concepts

Term Meaning
root A named directory (alias -> path) that Acervus indexes.
file A file discovered under a root, tracked by its path relative to it.
mark A label attached to files (many-to-many).
stack A named group a file can belong to.

Naming: acervus is the project, acre is the installed command, mark is a label, stack is a file group.

Requirements

  • Python 3.14+
  • Poetry for dependency management
  • mise for tooling and task running

Installation

# Install the toolchain and dependencies (mise reads mise.toml / pyproject.toml)
mise install
poetry install

This exposes the acre command (entry point acervus.inits.wiring:main).

Configuration

Acervus reads a TOML config from ~/.config/acervus/config.toml.

See config.example.toml:

[acervus]
db_path = "~/.local/share/acervus/acervus.db"
# ignore = [".git", ".venv", "node_modules", "__pycache__"]

[acervus.roots]
# docs   = "/home/user/docs"
# photos = "/home/user/photos"
  • db_path — where the SQLite index lives.
  • [acervus.roots] — the named directories Acervus manages (alias = "path").
  • ignore — glob patterns a scan skips, matched against one path component at a time: .venv skips a directory of that name at any depth, *.pyc skips a file. An ignored directory is pruned, not walked. Writing the key replaces the default list shown above; ignore = [] indexes everything.

If no config file is found, acre prints a hint and exits.

Usage

acre                 # launch the TUI

The app opens on the roots screen, listing each configured alias -> path.

Key Where Does
f / m / t anywhere Open the files, marks or stacks screen
q anywhere Quit
s roots Scan the root under the cursor and report what changed
enter / backspace files Open the directory under the cursor, go back up
r files Move to the next root
k / c files Cycle the filter by mark, by stack
space files Select the file under the cursor, or deselect it
a / x files Put a mark on every file selected, take one off
s / u files Put them in a stack, take them out of their stacks
escape any screen Back

The files screen browses one directory of one root at a time: subdirectories first, with how many files each holds, then the directory's own files. Nothing acts on a directory — marking a whole tree in one keypress is not offered. With nothing selected, the four operations are aimed at the file under the cursor. A directory's files are read a page at a time, so one holding hundreds of thousands draws at once and reads the rest as the cursor moves down.

A scan inserts files the root has and the index lacks, rewrites those whose size or mtime moved, and drops those the root no longer has. Marks and stacks come into being by being applied, and are deleted once nothing carries or sits in them.

Architecture

Acervus follows the GLIMPSE layered architecture, with import boundaries enforced by import-linter:

Layer Responsibility
pacts Protocols, DTOs, dataclasses, exceptions (no dependencies).
specs Pure business invariants, for mills only.
mills Pure business logic; takes dependencies via constructor.
links Data access — SQLAlchemy models, engine, repositories.
gates Entry points — the Textual TUI.
inits Config loading, dependency injection, the main() entry point.
edges Infrastructure boundary.

Only inits may wire links and gates together, and specs is reachable from mills alone. See CLAUDE.md for the full layer rules and conventions.

Source layout

src/acervus/
  pacts/               # protocols, DTOs, exceptions, AcervusConfig
  specs/               # business invariants
  mills/               # business logic
  links/db/sqlalchemy/ # models, engine, repositories, transaction
  links/fs/pathlib/    # the pathlib filesystem reader
  gates/tui/textual/   # the Textual app and its screens
  inits/               # config loading, DI, the entry point
  edges/               # infrastructure

Development

Tasks come from mise.toml and the shared config it includes over git. mise tasks ls --all lists them.

mise run test:py            # all tests
mise run test:unit          # unit tests only
mise run test:int           # integration tests only
mise run lint:py            # every linter at once — what CI runs
mise run lint:ruff          # ruff, --no-fix
mise run lint:mypy          # mypy src (strict)
mise run lint:import-linter # GLIMPSE layer contracts
mise run lint:pylint src
mise run lint:codespell
mise run format:py          # black, ruff --fix and taplo

Pass extra arguments through with --, which is how you run a single test:

mise run test:unit -- -k test_it_trims_surrounding_whitespace

Tooling: Black (line length 88, preview), Ruff (select = ["ALL"], preview), MyPy (strict), Import Linter, Pylint, Codespell. Unit tests mirror src/ and stay IO-free; integration tests exercise links, gates and the entry point against real infrastructure.

License

BSD 3-Clause. See LICENSE.

About

Filesystem tagging tool

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages