aboutsummaryrefslogtreecommitdiff

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

A rules file

One file per directory, in ~/.config/krino/dirs/:

(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

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.12.

go install git.labunix.xyz/krino/cmd/krino@latest
git clone https://git.labunix.xyz/krino.git          # or 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.