# krino design Status: draft for review, 2026-09-11. Target release: **0.0.1**. krino (from Greek κρίνω, "to separate, to judge, to decide") sorts files in chosen directories by rules. A rule tests a file's type, name, path, size, age, text content or duplicate status, combined with `and` / `or` / `not`, and applies actions: copy, move, rename, delete. krino shows what it will do, lets you approve all of it or file by file, logs every step, and can undo a run. It grew out of an earlier Python prototype; the lessons that shaped it are in §16. --- ## 1. Scope **0.0.1 is:** the engine and a command-line interface. - per-directory rule files, a main file listing which directories run, a template for new ones - conditions with `and` / `or` / `not` over type, name, path, content, size, age, duplicates - actions copy, move, rename, delete (to Trash by default), chained - plan, review, approve all or per file, `-y` to skip review - a log of every step, and `krino undo` - placeholders in destinations and new names - builds for Linux, FreeBSD and OpenBSD **Not in 0.0.1:** the GUI (a separate project after 0.0.1, see §12), watch mode, OCR, EXIF dates, macOS and Windows, a content cache, a shell-command action, looking inside archives. ## 2. Terms | Term | Meaning | |---|---| | directory | a configured directory krino sorts, e.g. `~/downloads`; one config file each | | root | that directory's path | | rule | a named condition plus actions | | test | one leaf of a condition, e.g. `(type pdf)` | | chain | the ordered actions krino will perform on one file | | plan | every chain for one directory, computed before anything changes | | run | one invocation of krino that applied at least one step, across all the directories it processed; has an id; the unit of undo | ## 3. Files and locations ``` $XDG_CONFIG_HOME/krino/ default ~/.config/krino krino.conf main file: which directories run, defaults, log path template.conf copied by `krino new` dirs/.conf one per directory $XDG_STATE_HOME/krino/ default ~/.local/state/krino krino.log the log (§9) .lock per-directory lock while a run is active ``` `krino init` creates the config directory with a commented `krino.conf` and `template.conf` (both embedded in the binary). It refuses if krino.conf exists and keeps an existing template.conf. `krino new NAME PATH` copies `template.conf` to `dirs/NAME.conf`, fills in the path, and appends NAME to `include` in `krino.conf`, keeping its comments. Generated files begin with `;; -*- mode: lisp -*-` and `;; vim: set ft=lisp :` so editors apply Lisp highlighting and parenthesis matching. ## 4. Config language ### 4.1 Syntax The files are s-expressions (see `docs/sexp-primer.md` for a tutorial). - **List:** `(` items separated by whitespace `)` - **String:** `"..."`. A backslash escapes only `"` and `\`; any other backslash is kept literally, so `"\bacme\b"` is the regex `\bacme\b`. Strings may span lines. - **Symbol:** any other run of characters except whitespace, `(`, `)`, `"`, `;`. - **Comment:** `;` to end of line. - Encoding UTF-8. No other syntax: no quote characters, no dotted pairs, no block comments. The reader records the byte offset, line and column of every list and atom. krino edits a file by splicing text at those offsets (`krino new` inserts a name into `include`), so everything else, comments and layout included, stays byte for byte. A printer for generated forms arrives with the GUI. Errors report `file:line:col`. An unclosed list is reported where it opened, described by its first atoms: `"(" never closed: (rule "acme" ...)`. Paths, keywords, regexes and rule names must be strings. Settings values, type names, operators, sizes and durations are symbols. ### 4.2 `krino.conf` ```lisp (include "downloads" "documents") ; dirs/.conf, run in this order (log "~/.local/state/krino/krino.log") ; optional (defaults ; optional; any setting from 4.4 (min-age 5m)) ``` ### 4.3 `dirs/.conf` ```lisp (path "~/downloads") ; required (recursive no) ; any setting from 4.4 (ignore "*.part" "*.aria2" ".*") ; may repeat; patterns accumulate in order (rule "acme" (when (type document) (or (content "acme ltd" "0000000000") (name "\bacme\b")) (not (name "^draft"))) (move "Work/Acme/{mtime:%Y}") (stop)) ``` Top-level forms may appear in any order, except that rules are evaluated in the order written. ### 4.4 Settings Each setting can appear in `(defaults ...)`, at the top of a directory file, or (where marked) inside a rule. The most specific wins: rule, then directory, then defaults, then built-in. | Setting | Values | Built-in | In a rule | |---|---|---|---| | `case` | `ignore` \| `strict` | `ignore` | yes | | `fold` | `yes` \| `no`. Strip diacritics before comparing: ą→a, ł→l, é→e, ü→u, and so on for all Latin letters | `yes` | yes | | `recursive` | `yes` \| `no` | `no` | | | `max-depth` | integer, 1 = root only | unlimited | | | `min-age` | duration. Skip files modified more recently | `2m` | | | `max-read` | size. No content extraction above this file size | `50M` | | | `busy` | suffixes. Skip `f` when `f` exists beside it | `".part" ".aria2" ".crdownload"` | | | `on-conflict` | `suffix` \| `skip` \| `overwrite` (§7.4) | `suffix` | yes | Durations: integer plus `s m h d w`. Sizes: integer plus optional `K M G T` (powers of 1024). ### 4.5 Rules ```lisp (rule NAME ITEM...) ``` Items, in any order except that actions run in the order written: | Item | Meaning | |---|---| | `(when COND...)` | the condition. Several conditions means all must hold. A rule without `when` matches every file. An empty `(when)` is an error. | | `(case ...)` `(fold ...)` `(on-conflict ...)` | rule-level settings | | `(copy DEST)` | copy the file into directory DEST | | `(move DEST)` | move the file into directory DEST | | `(rename NAME)` | rename in place; NAME must not contain `/` | | `(delete)` | move to Trash | | `(delete permanent)` | unlink; cannot be undone | | `(stop)` | once this rule matches, evaluate no further rules for this file | A rule with only `(stop)` is an exclusion: files it matches receive no actions from later rules. `DEST` is a directory: relative paths are relative to the root, `~` expands, absolute paths are allowed. It is created if missing. `DEST` and `NAME` take placeholders (§7.3). ## 5. Conditions ### 5.1 Operators `(and C...)`, `(or C...)`, `(not C)`. `and` and `or` take one or more arguments; `not` exactly one. ### 5.2 Tests | Test | True when | |---|---| | `(type T...)` | the name ends in `.T` for any T, case-insensitively. T may be a group (§A) or a multi-part suffix like `tar.gz`. | | `(name "RE"...)` | the file name matches any regex | | `(path "RE"...)` | the path relative to the root matches any regex | | `(content "KW"...)` | the extracted text contains any keyword (§6) | | `(size OP SIZE)` | OP is `> >= < <= =` | | `(age OP DURATION)` | age by modification time | | `(duplicate)` | another scanned file has identical content, and this one is not the original (§5.5) | | `(duplicate "DIR"...)` | the same, also comparing against files under those directories | | `(matched)` | an earlier rule matched this file | Regexes use Go's RE2 syntax: POSIX ERE plus `\d \w \s \b`, non-greedy quantifiers, and inline flags `(?i)` `(?-i)`; no backreferences or lookaround. ### 5.3 Case and diacritics `case` and `fold` apply to `name`, `path` and `content`, in both the file's data and the pattern. `type` is always case-insensitive. A regex can override `case` locally with `(?i)` or `(?-i)`. Folding decomposes letters (Unicode NFD), drops combining marks, and maps the letters that do not decompose (ł ø đ ħ ß æ œ and their capitals) to ASCII. ### 5.4 Evaluation 1. Every test sees **the original file**: name, path, size, mtime and content as they were when scanning. Actions from earlier rules do not change what later rules see. 2. Rules are evaluated in order. A matching rule adds its actions to the file's chain. `(stop)` on a matching rule ends evaluation for that file. 3. Tests have no side effects, so `and` / `or` evaluate their arguments cheapest first: `type`, `size`, `age` and `matched`, then `name` and `path`, then `duplicate`, then `content`. Content is extracted at most once per file, and only if evaluation reaches a `content` test. 4. If a file's text cannot be extracted, `content` is false and the plan shows a warning naming the rule that wanted it. ### 5.5 Duplicates Candidates are the scanned files plus, for `(duplicate "DIR"...)`, every regular file under those directories (symlinks skipped). Files are grouped by size. Only groups of two or more are hashed: first the first and last 64 KiB, then SHA-256 of the whole file. Empty files are never duplicates. The **original** in each group is, in order of preference: a file under one of the given DIRs, then the oldest by mtime, then the shortest name, then the name that sorts first. `(duplicate)` is true for every other scanned file in the group. ## 6. Content extraction | Format | Method | |---|---| | text | known text extensions, or detected from the first 8 KiB: valid UTF-8, or UTF-16 with a BOM, and no NUL bytes | | pdf | `pdftotext -q -enc UTF-8 FILE -`, 30 s timeout | | docx xlsx pptx odt ods odp epub | zip plus streaming XML, Go standard library | | html xml | tags removed, entities decoded | | doc | `antiword`, else `catdoc` | | xls / ppt | `xls2csv` / `catppt` (both from catdoc) | | anything else | no content | External tools are optional. Each is looked up once per run; `krino check` lists which were found. Files above `max-read` are not extracted. Tools run without a shell, get absolute paths only (so a name starting with `-` is never read as an option), and are killed at the timeout. Before matching, text and keywords are normalised the same way: case (if `ignore`), fold (if `yes`), and runs of whitespace collapsed to one space, so a keyword split across lines in a PDF still matches. Matching is substring: `"acme"` matches `"acmeco"`. Words hyphenated across lines in a PDF are not rejoined. ## 7. Actions ### 7.1 Chains A file's chain is the actions of every matching rule, in rule order, and within a rule in the order written. The executor tracks the file's current path: - `copy` leaves the file where it is; - `rename` changes its name, `move` its directory; later steps use the new path; - `delete` ends the chain. Steps after it appear in the plan as "skipped: deleted by rule X". The plan warns when a chain moves a file more than once; that is usually a missing `(stop)`. ### 7.2 How each action is carried out - **move:** `rename(2)` when source and destination are on the same filesystem. Otherwise: copy to a temporary file in the destination directory, fsync, rename into place, then remove the source. If any step fails the source is untouched and the temporary file removed. - **copy:** to a temporary file in the destination, then rename into place. Mode and mtime are preserved. - **rename:** `rename(2)` within the same directory. - **delete:** into the freedesktop.org Trash in `$XDG_DATA_HOME/Trash` (`files/` plus a `.trashinfo` in `info/`). A file on a different filesystem from the Trash is not trashed: the step fails with a message suggesting `(delete permanent)` or a move. The Trash layout is the same on Linux, FreeBSD and OpenBSD. - **delete permanent:** `unlink(2)`. Before each step the executor checks that the source still exists with the size and mtime recorded in the plan. If not, the step fails as "changed since plan" and the rest of that file's chain is skipped. ### 7.3 Placeholders | Placeholder | Value | |---|---| | `{name}` | the current file name | | `{stem}` | the name without its last extension | | `{ext}` | the last extension with its dot, e.g. `.pdf`; empty if none | | `{1}` … `{9}` | capture groups of the first `name` test that matched in this rule | | `{mtime:FMT}` | the file's modification time | | `{now:FMT}` | the start of the run | | `{{` `}}` | literal braces | FMT is a strftime subset: `%Y %m %d %H %M %S %j %%`. Captures come from the first `name` test, not inside a `not`, that was true while evaluating this rule. Tests of equal cost keep their written order, so "first" is well defined. `krino check` rejects a rule that uses `{N}` unless every such `name` test in it has at least N groups, and a rule that uses `{N}` with no `name` test at all. ### 7.4 Conflicts When the target already exists: - `suffix` (default): use `stem_1.ext`, `stem_2.ext`, ... - `skip`: skip this step; the chain continues from the file's current path - `overwrite`: move the existing target to Trash first (logged, so undo restores it), then proceed A `copy` whose target has identical content is skipped as "already there", whatever the policy, so a backup rule can run every time. A step whose target is the file itself — a rule whose `DEST` resolves to the directory the file already sits in, or a `rename` to the name it already has — is skipped as "already there" as well, whatever the policy. Without that the file's own existence would read as a conflict with itself and the plan would rename it to `stem_1` on every run. Conflicts between files in the same plan are resolved when planning, so the plan shows final names. An in-plan claim is never displaced: when `overwrite` finds a target another step of the same plan has already claimed, it takes the next free name instead, because that path will hold the other step's output by the time either runs, and displacing it would destroy that step's result. The claim set spans the whole run, so two configured directories cannot plan the same final name. The executor re-checks at execution time; if the name has to change, the log records the actual name. ## 8. Plan, review, approval ### 8.1 What is scanned - The root, recursively if `recursive yes`, to `max-depth`. - Symlinks are never followed; symlinked files are skipped. - `ignore` patterns use gitignore semantics: `*`, `**`, `?`, `[...]`; a leading `/` anchors to the root; a trailing `/` matches directories only; `!` re-includes; a pattern without `/` matches at any depth; the last matching pattern wins. Ignored directories are not descended into. - Always ignored: every rule `DEST` that lies inside the root, the Trash, and the config directory. For a `DEST` with placeholders, the part before the first placeholder is ignored: `Work/Acme/{mtime:%Y}` ignores `Work/Acme/`. Known limitation: a `DEST` whose *first* path component is itself a placeholder (`{ext}`, `{mtime:%Y}`) has no static prefix, so nothing can be excluded before the walk, and krino would re-examine its own output; plan 3 closes this by treating a file already at its computed destination as a no-op. - Skipped as busy: files newer than `min-age`, or with a `busy` sibling. ### 8.2 Display One directory at a time: ``` krino: downloads ~/downloads 266 scanned · 41 to act on · 2 warnings # file actions rule 1 scan001.pdf move → Work/Acme/2026/ acme content "acme ltd" 2 fv_123.pdf copy → ~/backup/invoices/2026/ backup content "invoice" move → Work/Acme/2026/ acme name \bacme\b 3 IMG_2031.JPG rename → 2026-09-01_IMG_2031.JPG photos type image move → Photos/2026/ 4 setup-1.2.deb DELETE permanently old-pkgs age 94d 5 report (1).pdf trash (duplicate of Work/report.pdf) refiled warnings brochure.doc content unreadable: antiword/catdoc not installed (rule acme) big.pdf content unreadable: 120M > max-read 50M not acted on: 3 busy · 12 ignored · 210 unmatched (-v lists them) [a] apply all [c] choose per file [s] skip this directory [q] quit ``` - Colours use the terminal's ANSI palette, only on a terminal, and never when `NO_COLOR` is set. Permanent deletes are always marked in capitals. - A plan taller than the terminal is shown through `$PAGER` (default `less -FRX`) and the prompt follows when the pager exits. - Keys are read one at a time without Enter. - `[s]` applies nothing in this directory and moves on to the next one. `[q]` stops krino; directories already applied in this run stay applied and can be undone with `krino undo`. ### 8.3 Choosing per file ``` [2/41] fv_123.pdf copy → ~/backup/invoices/2026/ move → Work/Acme/2026/ [y] yes [n] no [a] yes to this and all remaining [d] done, apply chosen so far [q] quit, apply nothing ``` Approval is per file: the whole chain or none of it. ### 8.4 Modes - default: plan, review, approve. - `-y`: print the plan and apply everything. - `-n`: print the plan and exit. `-n --json` prints it as JSON. - If stdin is not a terminal and neither `-y` nor `-n` is given, krino refuses rather than guess. ## 9. Log Append-only, `$XDG_STATE_HOME/krino/krino.log`, one line per event, tab-separated: ``` time run dir file step action status rule src dst size mtime detail ``` - `time`: RFC 3339 with offset. `run`: e.g. `20260911T100203-4f2a`. - `action`: `run-start`, `mkdir`, `copy`, `move`, `rename`, `trash`, `delete`, `displace` (a target trashed by `overwrite`), `run-end`, and the `undo-` forms of each. - `status`: `ok`, `failed`, `skipped`, `declined`. - `size` and `mtime` describe the file at `dst` after the step, so undo can tell whether it has been touched since. - Fields are escaped: `\t`, `\n`, `\\`, and `\xNN` for control bytes and bytes that are not valid UTF-8. Every file name round-trips exactly, and the file stays readable with `grep` and `awk`. Dry runs are not logged. Files never acted on (unmatched, ignored, busy) are not logged; declined files are. ## 10. Undo - `krino log` lists recent runs: id, time, directories, counts. - `krino undo` reverses the most recent run that has not been undone, in every directory it touched; `krino undo RUN` a specific one. Undo runs cannot themselves be undone. - Undo builds a plan like any other, shown and approved the same way (`-y` and `-n` apply). Reversals, last step first within each file: | Step | Reversal | Refused when | |---|---|---| | move, rename | move `dst` back to `src` | `dst` missing or changed (size/mtime), or `src` now exists | | copy | move `dst` to Trash | `dst` missing or changed | | trash | restore from Trash, remove the `.trashinfo` | the Trash entry is gone, or `src` now exists | | displace | restore the displaced target from Trash | as above | | mkdir | remove the directory if empty | not empty | | delete permanent | none; reported as not undoable | always | If a reversal is refused, the earlier steps of that file's chain are not reversed either, so no file is left half undone. Undo runs are logged like any other run. ## 11. Command line ``` krino [-y | -n] [-v] [--json] [-c FILE] [NAME...] plan and apply every included directory, or only the named ones krino init create ~/.config/krino with krino.conf and template.conf krino new NAME PATH create dirs/NAME.conf from the template and include it krino check [NAME...] validate config, list rules and available extractors krino explain FILE evaluate every rule against FILE, show each test's result krino log [-n N] list recent runs krino undo [RUN] reverse a run (default: the last one) -y apply without asking -n dry run: show the plan, change nothing -v also list unmatched, ignored and busy files; full match reasons --json with -n: the plan as JSON (format unstable before 1.0) -c FILE use FILE instead of ~/.config/krino/krino.conf -h, --help help --version print "krino 0.0.1" ``` Exit status: 0 success, including nothing to do and everything declined; 1 one or more steps failed; 2 usage or config error; 130 interrupted. A config error anywhere stops the whole run before any scanning: krino never acts on a config it only partly understood. Ctrl-C during apply finishes the current step, logs it, and stops. A second krino on the same directory waits for the lock, or fails immediately with `-y` (so a cron job never piles up). ## 12. Architecture One binary, frontends kept thin. Everything the GUI will need lives in the engine and returns data. ``` cmd/krino/ flags, subcommands, exit codes internal/sexp/ reader: syntax tree with positions and byte offsets internal/config/ syntax tree → typed config; validation; embedded template and defaults internal/cond/ condition tree: compile, cost ordering, evaluation, explain trace internal/scan/ walking, busy detection internal/ignore/ gitignore-compatible path matcher internal/extract/ text extraction internal/norm/ text normalisation: case, diacritics, white space internal/dup/ duplicate index internal/plan/ files × rules → chains; placeholders; conflicts; JSON internal/apply/ executing approved steps; cross-filesystem moves internal/trash/ freedesktop.org Trash internal/journal/ log writer and reader, run ids, undo planning internal/engine/ the facade: Load, Check, Plan, Apply, Explain, Runs, PlanUndo, NewDir, Init internal/tui/ terminal rendering, pager, key prompts ``` Rules for the GUI to come: 1. Every operation is an `engine` call that returns data. The CLI only renders it. 2. `Apply` takes a plan plus the set of approved files. The CLI's `a` and `c` and a GUI's checkboxes are the same call. 3. The reader records byte offsets, so a GUI can replace one form's text and leave the rest of the file, comments included, untouched. 4. `Explain` returns the condition tree with each test's value. 5. Log and undo are in the engine. ## 13. Performance - The walk uses `os.ReadDir` and prunes ignored directories. - Files are evaluated by a pool of `GOMAXPROCS` workers. `pdftotext` and the other tools run in parallel within that bound. - Cheap tests first; content only when reached; content at most once. - Keywords are normalised once at load time; matching is `strings.Contains` per keyword on normalised text. Aho-Corasick only if a benchmark shows keyword matching matters next to extraction. - Duplicates: stat-only grouping, partial hash, then full hash. - Measured before release on a real downloads directory of a few hundred files, about half of them PDFs, and reported in the release notes. Goal: a full plan with content rules in under 2 s on a 4-core laptop. `make bench` runs the Go benchmarks on generated trees. ## 14. Build, dependencies, release - Go 1.24 or newer. `CGO_ENABLED=0`: static binaries. - Go dependencies: `golang.org/x/term` (key-at-a-time input) and `golang.org/x/text` (Unicode normalisation for `fold`). Everything else is the standard library, including the s-expression reader and the gitignore matcher. - `make` builds; Go downloads the module dependencies itself. It then checks for the optional extractors and prints what is missing. - `make deps` installs the extractors with the system package manager, asking for privileges: `apt-get install poppler-utils catdoc antiword` (Debian, Devuan), `pkg install poppler-utils antiword` (FreeBSD; catdoc is left out because it pulls in tcl/tk), `pkg_add poppler-utils catdoc antiword` (OpenBSD). Plain `make` never asks for privileges. - Targets: `help build install uninstall test vet fmt lint ci bench cross release deps install-hooks`. `install` puts the binary in `$PREFIX/bin` (default `~/.local`), man pages in `$PREFIX/share/man`, examples in `$PREFIX/share/doc/krino/examples`. - `make cross`: linux/amd64, linux/arm64, freebsd/amd64, openbsd/amd64. - `make release VERSION=0.0.1`: runs `ci` and `cross`, writes tarballs and `SHA256SUMS` into `dist/`, and creates the annotated tag **`v0.0.1`** (the `v` is required for `go install ...@v0.0.1`). It never pushes. - Version stamped with `-ldflags -X main.version=...`. - `scripts/leak-check` keeps personal data out of the repository. `make ci` runs it, and so does the pre-commit hook that `make install-hooks` installs. It checks the staged files against built-in patterns (the current user's home directory, email addresses) and against a private pattern list whose path is set with `git config krino.leakpatterns FILE`. The list lives outside the repository, so the secrets it names are never published either. Without it, only the built-in patterns run. - `/local/` is ignored by git: a place for personal configs and test data next to the code. Repository contents for 0.0.1: `README.md` (60-second quickstart), `LICENSE`, `CHANGELOG.md`, `Makefile`, `go.mod`, `cmd/`, `internal/`, `scripts/`, `man/krino.1`, `man/krino.conf.5`, `docs/design.md`, `docs/sexp-primer.md`, `examples/` (by type, invoices by tax number with placeholder numbers, screenshots by date, cleaning up old installers), `testdata/`. ## 15. Testing - **Unit tests** per package, table-driven. - **sexp:** positions and offsets (every list's offsets point at its own parentheses), error positions, and a fuzz target for the reader. - **gitignore:** an oracle test that builds trees and compares every path against `git check-ignore --no-index` (skipped if git is absent). - **Conditions:** truth tables; a property test that random condition trees give the same result with and without cost reordering. - **Extraction:** small fixtures in `testdata/` for each format; PDF and legacy-format tests skip when the tool is missing. - **Plan, apply, undo:** in temporary directories: build a tree, plan, apply, check; undo; check the tree is identical to the start (paths, contents, modes, mtimes). A cross-filesystem move test runs when a tmpfs is available on a different device, and skips otherwise. - **Trash:** with `XDG_DATA_HOME` pointed at a temporary directory. - **CLI:** golden files for plan rendering without colour; exit codes. - **Parity check before release:** run `krino -n` beside the prototype's dry run over the same real directory, and explain every difference. The rules and the results stay outside the repository. `make ci` is the gate: gofmt, vet, tests, the leak check, man page lint. ## 16. Lessons from the prototype krino replaces a Python prototype that sorted a downloads folder by keyword. Its defects shaped these decisions: | Prototype | krino | |---|---| | filename keywords were substrings, so a short keyword matched inside unrelated words (`"art"` matched `start.pdf`) | name tests are regexes, e.g. `\bart\b` | | `.doc` and `.xls` content was never read (the libraries do not support those formats) | antiword or catdoc, or a warning | | code files were listed as supported, but their content was never read | any text file is read | | the rules lived in the repository beside the code | rules live in `~/.config/krino`, never in the repository (§14 leak check) | | it installed Python packages with `pip --break-system-packages` | a static binary; system packages via `make deps` | ## 17. Decisions and why | Decision | Why | |---|---| | Config in one central place | target directories stay clean; one place to back up; the tool never sorts its own config | | Every matching rule runs, `(stop)` to end | chains like copy-then-move need it; `stop` gives first-match behaviour where wanted | | Tests see the original file | the plan is fully known before approval, and files can be evaluated in parallel | | s-expressions, not TOML, YAML or INI | conditions are the core of the config; in sexp they are structure rather than a string in a second language. No nested quoting (TOML's `'''` around single-quoted regexes), no precedence, actions in written order. YAML turns `*` and `!` in ignore patterns into syntax; INI has no standard escaping | | `pdftotext` rather than a Go PDF library | best text quality of the open tools, fast, packaged on all three systems | | Delete to Trash by default | recoverable, and undo can restore it | | A new repository rather than the prototype's | the prototype's history holds personal data; the prototype keeps working until krino reaches parity | | Personal configuration never enters the repository | rules hold private data; the leak check enforces it on every commit and in `make ci` | ## 18. Settled before implementation 1. **Hosting: local only for now.** The module path is `krino`. When a remote is chosen, `go mod edit -module ` and one `sed` over the `"krino/internal/...` imports rename it; nothing else depends on it. `go install ...@v0.0.1` works only after that. 2. **License: GPL-3.0-or-later.** Full text in `LICENSE`; every source file carries `// SPDX-License-Identifier: GPL-3.0-or-later`. ## Appendix A: type groups | Group | Extensions | |---|---| | `image` | jpg jpeg png gif webp bmp tif tiff heic heif avif svg ico raw cr2 nef arw dng | | `video` | mp4 mkv webm mov avi m4v mpg mpeg wmv flv 3gp | | `audio` | mp3 flac ogg opus m4a aac wav wma aiff | | `archive` | zip tar gz tgz bz2 tbz2 xz txz zst 7z rar lz lzma cpio | | `document` | pdf doc docx odt rtf txt md tex | | `spreadsheet` | xls xlsx ods csv tsv | | `presentation` | ppt pptx odp | | `ebook` | epub mobi azw azw3 fb2 djvu | | `code` | go c h cpp hpp py sh js ts rs java rb pl lua html css json yaml yml toml xml sql | | `text` | txt md log csv tsv json yaml yml toml xml ini conf | | `package` | deb rpm apk appimage exe msi flatpak snap | | `font` | ttf otf woff woff2 | Groups overlap; a file can belong to several.