aboutsummaryrefslogtreecommitdiff
path: root/README.md
blob: 8b4b526ea143d7440f9bad4acc8da6b385b92d89 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# 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.**

```
git clone https://git.labunix.xyz/krino.git
go install git.labunix.xyz/krino/cmd/krino@latest    # from 0.0.12 on
```

`go install` needs a release carrying the module path below, so it works
from the next tag; 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.