dotcli: add dot setup folders (XDG short-name migration)

Adds a dot setup subcommand family (idempotent, re-runnable machine-setup
tasks) with a folders task that brings the 8 standard XDG user
directories under a fixed short-name convention (Desktop -> .desktop,
Documents -> doc, Downloads -> dwn, Music -> mus, Pictures -> pic,
Videos -> vid, Templates/Public -> .ignoreme), hardcoded rather than read
from ~/.config/user-dirs.dirs -- that file is a separate, manually
tracked dotfile whose values can drift or go stale, and the migration
must not depend on it being correct. The nested Pictures/Screenshots
folder is renamed to pic/screenshots in the same pass.

Content left behind in a legacy full-named folder by a fresh XDG-defaults
install -- empty or not -- is merged into its short-named replacement.
A same-named collision between a legacy folder and its target is never
overwritten: it is skipped, reported, and the legacy folder is left in
place rather than deleted while something in it could not be merged.
--dry-run previews what would move/skip without touching the filesystem.
xdg-user-dirs-update runs once afterward to notify running apps/portals.
This commit is contained in:
2026-07-06 16:19:06 -04:00
parent 540928f24f
commit 1fd8e7e773
11 changed files with 783 additions and 16 deletions

View File

@@ -0,0 +1,79 @@
---
spec: dot-setup-folders
blocked-by: 0008-folders-collision-handling
---
## What to build
Remove the `--yes` confirmation gate that 0007/0008 built: a legacy folder
with real content in it is migrated unconditionally now, the same as an
empty one, since the collision handling from 0008 already makes the merge
non-destructive on its own (a same-named entry is never overwritten, and the
legacy folder is kept whenever any collision occurred). The `--yes` gate
turned out to protect against a scenario collision handling already
prevents, while making the everyday case — a machine that already has real
files in `~/Documents`, `~/Pictures`, etc. — a silent no-op unless the flag
was remembered, which defeats the point of the task.
In its place:
- `dot setup folders` always attempts the merge for every legacy folder,
content or none.
- A new `--dry-run` flag replaces `--yes` in the flag slot: it reports what
would move and what would be skipped as a collision, without touching the
filesystem at all (no `mkdir`, no `mv`/`rmdir`, no `xdg-user-dirs-update`).
- A real (non-dry-run) run now reports what it moved per legacy folder
(e.g. `moved 12 entries from ~/Documents to ~/doc`), instead of staying
silent on success. A folder where nothing top-level moved (already empty,
or everything in it collided) prints no such line — only non-trivial moves
and collisions produce output.
- `--yes` is removed outright (not kept as a silent no-op): passing it now
fails with argparse's standard unknown-option error.
## Acceptance criteria
- [x] A legacy folder with real content merges on a plain `dot setup
folders`, with no flag required
- [x] A real run prints `moved N entries from ~/<legacy> to ~/<target>` for a
folder where top-level entries actually moved, and nothing for a
folder where none did
- [x] A real run prints a dedicated line when the nested Screenshots folder
itself is moved (e.g. `moved ~/Pictures/Screenshots to ~/pic/screenshots`)
- [x] Collision detection/reporting and the "leave the legacy folder in
place when a collision occurred" behavior from 0008 are unchanged
under the new unconditional default
- [x] `--dry-run` reports the same would-move/would-skip information without
creating any target directory, moving/removing anything, or invoking
`xdg-user-dirs-update`
- [x] `dot setup folders --yes` fails with an unknown-option error (argparse
default), rather than being silently accepted or gated on
- [x] `dot setup folders help` output no longer mentions `--yes` and
documents `--dry-run` instead
- [x] Idempotency holds: re-running after a clean merge, and re-running
after a collision was reported, both behave the same as before
- [x] `~/.config/dot/tests/dot.fish` is updated to exercise the above
(replacing the old `--yes`-gated cases) and
`fishtape ~/.config/dot/tests/dot.fish` passes
## Implementation Notes
- The `--yes` gate and the `screenshots_emptyish`/`other_entries` machinery
that computed it were deleted outright rather than special-cased away:
once merging is unconditional, that machinery had no remaining purpose
(it existed solely to decide "empty enough to skip the gate").
- `mkdir -p $target_path` and the final `xdg-user-dirs-update` are both now
guarded by `not set -q _flag_dry_run`, making `--dry-run` a true no-op
rather than "no-op except for directory scaffolding."
- Collision detection (`test -e $target_path/...`) runs identically in both
modes; `--dry-run` only gates the actual `mv`/`rmdir`/`mkdir` calls, so the
reported would-move/would-skip split is exactly what a real run would do.
- Success reporting is per-legacy-folder and suppressed at zero: a folder
that was already empty (or whose only entries all collided) prints
nothing, so a routine re-run stays quiet like before.
- All prior collision/idempotency/Screenshots test scenarios were kept,
just re-pointed at the plain `dot setup folders` invocation instead of
`--yes`; two scenarios that only differed by which code branch (`--yes`
vs. silent-empty) they exercised now hit the same branch, but were both
kept since they still cover distinct fixture shapes (Pictures with vs.
without unrelated top-level content alongside a colliding Screenshots).
- `fishtape ~/.config/dot/tests/dot.fish` passes (178 tests).