diff options
| -rw-r--r-- | README.md | 328 | ||||
| -rw-r--r-- | docs/krino-gui.png | bin | 0 -> 70348 bytes |
2 files changed, 131 insertions, 197 deletions
@@ -1,103 +1,112 @@ # krino -krino sorts the files in one or more directories by rules: conditions on a -file's type, name, path, size, age, text content or duplicate status, -combined with `and` / `or` / `not`, that trigger actions — copy, move, -rename, delete (to the Trash by default), chained in the order written. It -shows the plan before touching anything, lets you approve all of it or file -by file, logs every step, and can undo a run. Version 0.0.9 is the engine -and this command-line interface: per-directory rule files, placeholders in -destinations and new names, a keyword cache that makes a rerun fast, and -builds for Linux, FreeBSD and OpenBSD. Version 0.0.11 has `krino-gui`, an -optional GTK 4 window onto the same engine (see below). It is not a watch -mode, OCR, EXIF dates, macOS or Windows support, a shell-command action, or -a way to look inside archives. +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. -## Install +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. -``` -make && make install -``` + -`make` builds `./krino`; Go fetches the module dependencies itself, and the -Go toolchain `go.mod` names (on OpenBSD, whose `go` package defaults to -`GOTOOLCHAIN=local`, run `GOTOOLCHAIN=auto make`). `make -install` puts the binary in `$PREFIX/bin` (default `~/.local/bin`), the man -pages in `$PREFIX/share/man`, and the examples and `docs/sexp-primer.md` in -`$PREFIX/share/doc/krino`. +## A rules file -A release tarball holds the built binary, the man pages, the examples and -this README, but no Makefile and no source; install it by hand: +One file per directory, in `~/.config/krino/dirs/`: -``` -install -m 755 krino ~/.local/bin/ -install -m 644 krino.1 ~/.local/share/man/man1/ -install -m 644 krino.conf.5 ~/.local/share/man/man5/ -``` +```lisp +(path "~/Downloads") -Some `(content ...)` tests need external extractors — `pdftotext` for PDF, -`antiword`/`catdoc` for legacy Word, `xls2csv`/`catppt` for legacy Excel and -PowerPoint. `make build` reports which of these are missing; install them -with: +;; Never touch these, whatever the rules below say. +(exclude (name "^keep-")) -``` -make deps +(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)) ``` -`make deps` is the only target that asks for privileges — it calls the -system package manager (`apt-get`, `pkg`, or `pkg_add`, depending on the -OS). Plain `make` never does. +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. -## The 60-second quickstart +## What it can do -Everything below happens in `/tmp/krino-demo` and a scratch config under -`/tmp/krino-xdg`, so it never touches a real directory or your real krino -config. Copy-paste it as written and your output should match what's pasted -here, except for the run id and timestamps, which are different every time. +- **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. -Create a handful of files of different types: +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 ``` -mkdir -p /tmp/krino-demo -cd /tmp/krino-demo -for f in acme-invoice.pdf photo.jpg podcast.mp3 archive.zip app.deb notes.txt; do - printf 'demo file: %s\n' "$f" >"$f" -done +make && make install ``` -Point krino at a scratch config for this walkthrough: +`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 -``` - -Set it up: - -``` -$ krino init -created /tmp/krino-xdg/config/krino/krino.conf -created /tmp/krino-xdg/config/krino/template.conf -next: krino new NAME PATH, for example: krino new downloads ~/Downloads -$ krino new demo /tmp/krino-demo -created /tmp/krino-xdg/config/krino/dirs/demo.conf and added "demo" to include -edit its rules, then check them with: krino check demo -``` -`krino new` copied the template to `dirs/demo.conf` and filled in the path. -Replace its body with a small rule. The built-in `min-age` is 2 minutes -(files skip as "too new" while they might still be downloading), which the -demo files we just created are younger than, so this also turns it down to -0 for this directory (`--min-age 0` would do the same for a single run): - -``` +krino init +krino new demo /tmp/krino-demo cat > "$XDG_CONFIG_HOME/krino/dirs/demo.conf" <<'EOF' -;; -*- mode: lisp -*- -;; vim: set ft=lisp : - (path "/tmp/krino-demo") -(min-age 0s) ; demo files are seconds old; built-in is 2m +(min-age 0s) ; these files are seconds old; the built-in is 2m (rule "acme" (when (name "acme")) @@ -105,84 +114,33 @@ cat > "$XDG_CONFIG_HOME/krino/dirs/demo.conf" <<'EOF' EOF ``` -This one rule matches any file whose name contains "acme" and moves it into -`Filed/Acme`. See what it would do, without moving anything: +`krino -n demo` prints the plan and changes nothing: ``` -$ krino -n demo krino: demo /tmp/krino-demo -6 scanned · 1 to act on · 0 warnings · 0.00s +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: 5 unmatched (-v lists them) +not acted on: 3 unmatched (-v lists them) ``` -On a terminal each block is wrapped to its width. A plan taller than the -terminal opens in your pager; `-P` prints it straight out instead, and -`krino -n > plan.txt` saves it as plain text, one field per line. - -Without `-n` or `-y`, `krino demo` shows the same plan and then asks, one -key at a time, no Enter needed: +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: ``` -[a] apply all [c] choose per file [s] skip this directory [q] quit -``` - -`c` asks about each file in turn, showing the same block as the plan above: - -``` -[1/1] acme-invoice.pdf - move → Filed/Acme/ - rule acme - because name "acme" - [y] yes [n] no [a] yes to this and all remaining [t] trash [d] delete permanently [w] write, apply chosen so far [q] quit, apply nothing -``` - -Approval is per file: a file's whole chain runs, or none of it. `t` sends -the file to the Trash and `d` deletes it permanently (after a `y`) instead -of what the rules planned. Each choice is echoed in red. `w` applies what you -chose so far and quits, so a long review can be finished another day. A README -can't paste a session it didn't actually run in a terminal, so here we -apply directly with `-y`, which shows the same plan and applies it without -asking: - -``` -$ krino -y demo -krino: demo /tmp/krino-demo -6 scanned · 1 to act on · 0 warnings · 0.00s - - 1 acme-invoice.pdf - move → Filed/Acme/ - rule acme - because name "acme" - -not acted on: 5 unmatched (-v lists them) +$ krino -y demo # the plan above, and then: 1 applied · 0 failed · 0 declined -``` - -`acme-invoice.pdf` is now in `Filed/Acme/`. Every run is logged: - -``` $ krino log -20260913T005509-db14 2026-09-13 00:55 demo 1 moved -``` - -And every run can be undone — `undo` alone would show its own plan and ask -the same way, with a menu of its own: - -``` -[a] apply all [c] choose per file [s] skip [q] quit -``` - -`-y` applies it directly: - -``` +20260917T115059-6df2 2026-09-17 11:50 demo 1 moved $ krino undo -y -krino: undo 20260913T005509-db14 +krino: undo 20260917T115059-6df2 1 files · 1 to reverse · 0 refused # file steps @@ -192,82 +150,58 @@ krino: undo 20260913T005509-db14 1 applied · 0 failed · 0 declined ``` -`/tmp/krino-demo` is back exactly as the first `printf` loop left it — that -is what `krino undo` is for. Delete both scratch directories when you're -done: `rm -rf /tmp/krino-demo /tmp/krino-xdg`. - -## Configuration - -Rules are s-expressions. `docs/sexp-primer.md` is a short tutorial on the -syntax with no Lisp background assumed; `krino.conf(5)` (`man/krino.conf.5` -in the source, installed as a man page) is the reference for every form, -setting and condition. `examples/` has four worked rule files: by type, by -content (invoices), by age (old installers), and renaming (screenshots). - -Rules live in `$XDG_CONFIG_HOME/krino` (default `~/.config/krino`) — -**never in this repository**. A leak check guards that: it runs in `make ci`, -and on every commit only after `make install-hooks`. It refuses staged files -that contain your home directory's path or an email address, and it catches -rule content only when a private pattern list is configured with -`git config krino.leakpatterns FILE`. +`/tmp/krino-demo` is back as it was. Clean up with +`rm -rf /tmp/krino-demo /tmp/krino-xdg`. ## The window -`krino-gui` is an optional GTK 4 window onto the same engine: it plans a -directory, shows what would happen to each file with the explanation beside -it, applies the files 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 in the log that `krino undo` can reverse. - ``` make gui && make install-gui ``` -It lives in `gui/`, a nested module of its own, and needs GTK 4 development -files and cgo — which the command line does not, so neither is needed to -build or use `krino` itself: +`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 ``` -The release tarballs hold the cross-compiled `krino` only; `krino-gui` is -built from source on the machine that runs it. `man/krino-gui.1` describes -the window, and `docs/gui-checklist.md` is what is run by hand before a -release that changes it. - ## Safety -- `(delete)` moves a file to the freedesktop.org Trash by default, not - `unlink(2)` — recoverable, by hand or with `krino undo`. `(delete - permanent)` is the explicit opt-out, and undo can never reverse it. -- Duplicates are found, never deleted. A rule can combine `(duplicate)` with - `move`, never with `delete`, and no other rule can delete a file krino has - found to be a duplicate. Move duplicates aside, for example to `~/.dupes/`, - and delete them later yourself or with a tool such as jdupes. -- Every step of every run is appended to `$XDG_STATE_HOME/krino/krino.log` - (default `~/.local/state/krino/krino.log`), tab-separated and - `grep`-able. -- `krino undo` reverses the most recent run, or a specific one by id from - `krino log`. A reversal that the world has moved on since (the target is - gone, changed, or the original path is occupied again) is refused for - that file rather than guessed at. -- `-n` prints the plan and changes none of your files — no config is - written, nothing is moved, nothing is logged. It may create krino's empty - state directory and refresh the keyword cache in `~/.cache/krino`, which - only makes the next run faster. +- `(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, local only.** The module path is `krino`; `go install` only -works once a remote is chosen and the imports are renamed to match -(`docs/design.md`, §18). Until then, build from a clone. -`krino -n --json`'s output shape is unstable before krino 1.0 — don't -script against it yet. +**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. -## License +## Licence -Everything in this repository — code, man pages, examples and the embedded -templates — is GPL-3.0-or-later. Full text in `LICENSE`. Go files, shell -scripts and man pages carry an SPDX identifier. +GPL-3.0-or-later, in full in `LICENSE`. By Lukasz Kasprzak — +contact@labunix.xyz. diff --git a/docs/krino-gui.png b/docs/krino-gui.png Binary files differnew file mode 100644 index 0000000..c7329c6 --- /dev/null +++ b/docs/krino-gui.png |
