# krino krino sorts a directory by rules you write. It reads the files, works out what belongs where, shows you the plan, and touches nothing until you say so. Every run is logged, and any run can be undone. Rules are conditions — a file's type, name, path, size, age, text content, or whether it is a duplicate of something you already have — combined with `and`, `or` and `not`, and actions: move, copy, rename, delete. There is a command line and an optional GTK 4 window onto the same engine. ![The plan, before anything has been touched](docs/krino-gui.png) ## A rules file One file per directory, in `~/.config/krino/dirs/`: ```lisp (path "~/Downloads") ;; Never touch these, whatever the rules below say. (exclude (name "^keep-")) (rule "invoices" (when (type document) (content "invoice" "faktura")) (move "Filed/Invoices/{mtime:%Y}") (stop)) (rule "screenshots" (when (and (type image) (name "^screenshot-([0-9-]+)"))) (rename "screen {1}.png") (move "Filed/Pictures") (stop)) (rule "copies I already have" (when (duplicate "~/Documents")) (move "~/.dupes") (stop)) (rule "old installers" (when (and (type iso exe) (age > 90d))) (delete)) ``` Conditions in a `(when ...)` must all hold; a `(content ...)` is true when any of its keywords appears. `{mtime:%Y}` and `{1}` are placeholders — the file's year, and the first capture group of the name test. `(stop)` means a file this rule matched takes no later rule. Four worked files are in `examples/`, and `krino.conf(5)` is the reference for every form. ## What it can do - **Conditions:** `type` (by extension or by kind — `image`, `document`, `archive`), `name` and `path` (regular expressions, with capture groups), `size`, `age`, `content` (text pulled out of PDFs and office documents), `duplicate` (the same bytes as a file somewhere else), and `matched` (what an earlier rule decided). Combined with `and`, `or`, `not`. - **Actions,** chained in the order written: `move`, `copy`, `rename`, `delete`. Placeholders fill in the file's name, its date, or a capture group from the rule that matched it. - **A plan first.** `-n` prints what would happen and changes nothing. Without it krino asks — all of it, or file by file, one key each. - **Undo.** `krino undo` reverses the last run, or one named by id. A reversal the world has moved on from is refused for that file, not guessed at. - **A log.** Every step of every run, tab-separated, in `~/.local/state/krino/krino.log`. - **Fail-closed.** A PDF whose text cannot be read does not match a content test, and so is left alone rather than filed by a guess. - **Fast on a rerun.** Extracted keywords are cached, so a second pass over a large directory does not re-read every document. It is not a watch mode, OCR, EXIF dates, a shell-command action, a way to look inside archives, or macOS and Windows support. ## Install ``` make && make install ``` `make install` puts the binary in `$PREFIX/bin` (default `~/.local/bin`), the man pages in `$PREFIX/share/man`, and the examples and a short s-expression tutorial in `$PREFIX/share/doc/krino`. On OpenBSD, whose Go package defaults to `GOTOOLCHAIN=local`, run `GOTOOLCHAIN=auto make`. Some `(content ...)` tests need external extractors — `pdftotext` for PDF, `antiword` or `catdoc` for legacy Word, `xls2csv` and `catppt` for legacy Excel and PowerPoint. `make build` says which are missing, and `make deps` installs them with the system package manager. That is the only target that asks for privileges; plain `make` never does. ## Try it in a scratch directory ```sh mkdir -p /tmp/krino-demo && cd /tmp/krino-demo for f in acme-invoice.pdf photo.jpg podcast.mp3 notes.txt; do printf 'demo file: %s\n' "$f" >"$f" done export XDG_CONFIG_HOME=/tmp/krino-xdg/config export XDG_STATE_HOME=/tmp/krino-xdg/state export XDG_DATA_HOME=/tmp/krino-xdg/data krino init krino new demo /tmp/krino-demo cat > "$XDG_CONFIG_HOME/krino/dirs/demo.conf" <<'EOF' (path "/tmp/krino-demo") (min-age 0s) ; these files are seconds old; the built-in is 2m (rule "acme" (when (name "acme")) (move "Filed/Acme")) EOF ``` `krino -n demo` prints the plan and changes nothing: ``` krino: demo /tmp/krino-demo 4 scanned · 1 to act on · 0 warnings · 0.00s 1 acme-invoice.pdf move → Filed/Acme/ rule acme because name "acme" not acted on: 3 unmatched (-v lists them) ``` Without `-n` krino shows that plan and asks — `[a] apply all`, `[c] choose per file`, `[s] skip this directory`, `[q] quit` — and under `c`, each file in turn, where `t` sends it to the Trash and `d` deletes it instead of what the rules planned. `-y` applies without asking, which is what a README can honestly paste: ``` $ krino -y demo # the plan above, and then: 1 applied · 0 failed · 0 declined $ krino log 20260917T115059-6df2 2026-09-17 11:50 demo 1 moved $ krino undo -y krino: undo 20260917T115059-6df2 1 files · 1 to reverse · 0 refused # file steps 1 demo/acme-invoice.pdf undo-move → /tmp/krino-demo/acme-invoice.pdf undo-mkdir /tmp/krino-demo/Filed/Acme undo-mkdir /tmp/krino-demo/Filed 1 applied · 0 failed · 0 declined ``` `/tmp/krino-demo` is back as it was. Clean up with `rm -rf /tmp/krino-demo /tmp/krino-xdg`. ## The window ``` make gui && make install-gui ``` `krino-gui` plans a directory, shows what would happen to each file with the reason beside it and the file itself below, applies the ones you check, undoes a run, and edits a directory's rules as forms or as text with the configuration checked as you type. Every decision is the engine's, so the window and the command line agree, and a run it applies is an ordinary run that `krino undo` can reverse. It lives in `gui/`, a nested module, and needs GTK 4 development files and cgo — which the command line does not, so neither is needed to build or use `krino` itself: ``` sudo apt install libgtk-4-dev libgirepository1.0-dev # Debian, Devuan ``` ## Safety - `(delete)` moves a file to the freedesktop.org Trash, not `unlink(2)`. `(delete permanent)` is the explicit opt-out, and undo can never reverse it. - Duplicates are found, never deleted. A rule may combine `(duplicate)` with `move` but never with `delete`, and no other rule may delete a file krino has found to be a duplicate. Move copies aside and delete them yourself, or with a tool such as jdupes. - A directory is locked while krino works on it, so two runs cannot fight over the same files. - Your rules live in `~/.config/krino`, never in this repository. A leak check in `make ci` refuses staged files holding a home directory path or an email address, and, with `git config krino.leakpatterns FILE`, any pattern of your own. ## Status **0.0.11.** The module path is `krino`; `go install` will work once the repository has a public URL and the imports are renamed to match. Until then, build from a clone. The shape of `krino -n --json` is unstable before 1.0 — don't script against it yet. Builds and tests are run on Linux, FreeBSD and OpenBSD. The release tarballs hold the cross-compiled command line only; the window is built on the machine that runs it. ## Licence GPL-3.0-or-later, in full in `LICENSE`. By Lukasz Kasprzak — lukas@labunix.xyz.