docs: enhance README and homepage with two screenshot plates
- Add assets/before-after.png — Plate i: five real renames laid out as a
typeset table; mulberry highlight on the description segment; visual
callouts for the U+202F gap and the user-keyword preservation case
- Add assets/session.png — Plate ii: editorial paper frame around an
ink-dark Claude Code session card showing user prompt, claude
orchestration, parallel Haiku fan-out across ten batches, and the run
receipt
- Keep the source HTMLs (assets/{before-after,session}.html) so the
plates can be regenerated via headless Brave at 1600x1100
- README.md rewritten: centered hero with embedded plate i, dedicated
"A session, end to end" section embedding plate ii, new highlights
(multi-prefix, idempotent), new "What the parser accepts" table,
gotchas extended to 13, real-world impact promoted to a 3-row table
- docs/index.html surgically extended (existing editorial CSS preserved):
new .plate and .additions components, nav gets what's-new and session
links, new section 04 "What's new" with three additions cards plus a
run-iii receipt, original receipt relabeled run-ii, gotchas list
extended with #11-13, new section 07 "The session" embedding plate ii,
install renumbered to 08
- Image refs in docs/index.html use absolute gitea raw URLs so they
resolve when served from gitea pages or viewed locally
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,20 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
# screenshot-rename
|
# screenshot-rename
|
||||||
|
|
||||||
> A Claude Code skill that turns a folder of timestamp-named screenshots into a folder of human-readable, searchable filenames — using parallel Haiku vision agents.
|
*A Claude Code skill that turns a folder of timestamp-named screenshots*<br>
|
||||||
|
*into a folder of human-readable, searchable filenames —*<br>
|
||||||
|
*using parallel Haiku vision agents.*
|
||||||
|
|
||||||
**Documentation:** [pages.tojo.team/cardinale/screenshot-rename](https://pages.tojo.team/cardinale/screenshot-rename/) — full homepage with the workflow, gotchas, and use-case worked examples.
|
[**Homepage**](https://pages.tojo.team/cardinale/screenshot-rename/) · [SKILL.md](SKILL.md) · [pipeline.py](pipeline.py) · [MIT](LICENSE)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<img src="assets/before-after.png" alt="Before / after — five real renames, including U+202F handling and user-keyword preservation" width="900">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
```
|
```
|
||||||
CleanShot 2026-04-15 at 09.14.07.png
|
CleanShot 2026-04-15 at 09.14.07.png
|
||||||
@@ -10,28 +22,41 @@ CleanShot 2026-04-15 at 09.14.07.png
|
|||||||
CleanShot - Shamel Studio Affiliate Referral Code Modal - 2026-04-15 at 09.14.07.png
|
CleanShot - Shamel Studio Affiliate Referral Code Modal - 2026-04-15 at 09.14.07.png
|
||||||
```
|
```
|
||||||
|
|
||||||
Built for [CleanShot](https://cleanshot.com)-style screenshot folders, but works on any directory of `.png` / `.gif` / `.mp4` / `.pdf` files named only by timestamp.
|
Built for [CleanShot](https://cleanshot.com)-style screenshot folders, but works on any directory of `.png` / `.gif` / `.mp4` / `.pdf` files named only by timestamp. Recognizes both **`CleanShot ...`** and Apple **`Screenshot ...`** filenames in the same pass, preserves any leading user-typed keyword prefix, and is safe to re-run on a folder that's already partially renamed.
|
||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
|
|
||||||
- **Parallel** — describes ~200 files in 3 minutes using 10 concurrent Haiku subagents.
|
- **Parallel** — describes ~200 files in 3 minutes using 10 concurrent Haiku subagents.
|
||||||
- **Safe** — pre-builds the full rename plan in memory, validates uniqueness and target collisions, then renames atomically with file-count audit. Designed after losing 4 files to a `mv` overwrite during prototyping.
|
- **Safe** — pre-builds the full rename plan in memory, validates uniqueness and target collisions, then renames atomically with a file-count audit. Designed after losing 4 files to a `mv` overwrite during prototyping.
|
||||||
|
- **Multi-prefix** — same pipeline handles `CleanShot ...`, Apple `Screenshot ...`, and files with hand-typed leading keywords (e.g. `jojo travel CleanShot ...`).
|
||||||
|
- **Idempotent** — re-running on a folder skips files already in the renamed `App - Description - timestamp.ext` form. No description-stacking.
|
||||||
- **Handles video / PDF** — extracts the first frame so vision agents can describe them.
|
- **Handles video / PDF** — extracts the first frame so vision agents can describe them.
|
||||||
- **Resizes for the vision tool** — Retina screenshots exceed Read's image cap; pipeline downsamples to 1568 px max.
|
- **Resizes for the vision tool** — Retina screenshots exceed Read's image cap; pipeline downsamples to 1568 px max.
|
||||||
|
|
||||||
|
## A session, end to end
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="assets/session.png" alt="Claude Code session — user prompt, parallel Haiku fan-out across ten batches, receipt of the run" width="900">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
The skill activates when you ask Claude conversationally. Behind the scenes it preps the folder, fans out ten Haiku agents in a single round-trip, validates the resulting plan, then applies the renames in a single Python pass with a file-count audit at the end.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
This is a Claude Code skill. Drop the `screenshot-rename/` directory into `~/.claude/skills/`:
|
This is a Claude Code skill. Drop the repo into `~/.claude/skills/`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://gitea.tojo.team/cardinale/screenshot-rename.git ~/.claude/skills/screenshot-rename
|
git clone https://gitea.tojo.team/cardinale/screenshot-rename.git \
|
||||||
|
~/.claude/skills/screenshot-rename
|
||||||
```
|
```
|
||||||
|
|
||||||
In your next Claude Code session, ask:
|
In your next Claude Code session, ask:
|
||||||
|
|
||||||
> rename all the cleanshot files in `~/Documents/Screenshots/` based on their content
|
> rename all the cleanshot files in `~/Documents/Screenshots/` based on their content
|
||||||
|
|
||||||
The skill will activate automatically.
|
The skill will activate automatically from its description.
|
||||||
|
|
||||||
## Usage from the command line
|
## Usage from the command line
|
||||||
|
|
||||||
@@ -52,19 +77,22 @@ python3 pipeline.py plan --src "/path/to/folder"
|
|||||||
python3 pipeline.py execute --src "/path/to/folder"
|
python3 pipeline.py execute --src "/path/to/folder"
|
||||||
```
|
```
|
||||||
|
|
||||||
The dispatch step (#2) currently requires a Claude Code session. See [Roadmap](#roadmap).
|
The dispatch step (#2) currently requires a Claude Code session.
|
||||||
|
|
||||||
## Documentation
|
## What the parser accepts
|
||||||
|
|
||||||
- **Homepage with worked examples:** [docs/index.html](docs/index.html)
|
| Form | Recognized | Becomes |
|
||||||
- **Full skill spec:** [SKILL.md](SKILL.md)
|
|---|---|---|
|
||||||
- **Pipeline source:** [pipeline.py](pipeline.py)
|
| `CleanShot 2026-MM-DD at HH.MM.SS.png` | yes | `CleanShot - <description> - 2026-MM-DD at HH.MM.SS.png` |
|
||||||
|
| `Screenshot 2026-MM-DD at H.MM.SS PM.png` (with U+202F) | yes — U+202F normalized to ASCII space | `Screenshot - <description> - 2026-MM-DD at H.MM.SS PM.png` |
|
||||||
|
| `<keywords> CleanShot 2026-MM-DD at HH.MM.SS.png` | yes — keywords title-cased and prepended to the AI description | `CleanShot - <Keywords + description> - 2026-MM-DD at HH.MM.SS.png` |
|
||||||
|
| `App - <description> - 2026-MM-DD at HH.MM.SS.png` | already renamed → **skipped** | (unchanged) |
|
||||||
|
|
||||||
## The gotchas this skill encodes
|
## The gotchas this skill encodes
|
||||||
|
|
||||||
This skill exists because every one of these caused real damage during development:
|
This skill exists because every one of these caused real damage during development:
|
||||||
|
|
||||||
1. The macOS `Read` tool has an image-size cap. Resize first.
|
1. The `Read` tool has an image-size cap. Resize first.
|
||||||
2. Vision can't read `.mp4` or multi-page `.pdf` directly. Extract a frame.
|
2. Vision can't read `.mp4` or multi-page `.pdf` directly. Extract a frame.
|
||||||
3. **Bash regex `[[ =~ ]]` does NOT populate `BASH_REMATCH` in zsh.** Targets become empty. Loops collide on the same filename. Files vanish. Use Python for any filename mutation.
|
3. **Bash regex `[[ =~ ]]` does NOT populate `BASH_REMATCH` in zsh.** Targets become empty. Loops collide on the same filename. Files vanish. Use Python for any filename mutation.
|
||||||
4. `mv` silently overwrites. Use `mv -n` or `os.rename` with explicit pre-existence check.
|
4. `mv` silently overwrites. Use `mv -n` or `os.rename` with explicit pre-existence check.
|
||||||
@@ -74,9 +102,20 @@ This skill exists because every one of these caused real damage during developme
|
|||||||
8. `Bash run_in_background` may exit early on `while read` loops. Run renames foreground via Python.
|
8. `Bash run_in_background` may exit early on `while read` loops. Run renames foreground via Python.
|
||||||
9. Haiku occasionally returns the resized `.jpg` filename instead of the original `.png`. Validator must try alt extensions.
|
9. Haiku occasionally returns the resized `.jpg` filename instead of the original `.png`. Validator must try alt extensions.
|
||||||
10. Always preserve the original `.mp4` / `.pdf` extension — describe via the extracted frame, rename the source.
|
10. Always preserve the original `.mp4` / `.pdf` extension — describe via the extracted frame, rename the source.
|
||||||
|
11. **macOS `Screenshot` filenames contain U+202F (NARROW NO-BREAK SPACE)** before AM/PM. Haiku echoes it as ASCII space, so a verbatim filename lookup misses every Screenshot file. Normalize on both sides of the lookup; emit ASCII space in the new name.
|
||||||
|
12. **Re-running is only safe if the parser skips already-renamed files.** Detect `^App - .+ - timestamp.ext$` and exclude.
|
||||||
|
13. **Leading user-typed keyword prefix is signal, not noise.** Title-case the keywords and prepend them to the AI description before assembling the new name.
|
||||||
|
|
||||||
The full discussion is in [SKILL.md](SKILL.md#the-critical-gotchas-every-one-of-these-caused-real-pain).
|
The full discussion is in [SKILL.md](SKILL.md#the-critical-gotchas-every-one-of-these-caused-real-pain).
|
||||||
|
|
||||||
|
## Real-world impact
|
||||||
|
|
||||||
|
| Run | Files | What happened |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 196 CleanShot | Lost 4 to the bash-regex-in-zsh gotcha (#3). |
|
||||||
|
| 2 | 196 CleanShot | Rebuilt with Python and `mv -n` — 189 renamed cleanly, zero loss. |
|
||||||
|
| 3 | 20 mixed (CleanShot + Apple Screenshot + one user-prefixed) | First plan attempt dropped every Screenshot file with a misleading `NO_DESC` error. Diagnosed the U+202F gotcha (#11) via `repr()` of the live filename. After adding U+202F normalization, multi-prefix support, and keyword preservation — all 20 renamed in one pass. |
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- Direct Anthropic API mode (no Claude Code session required) — needs `ANTHROPIC_API_KEY`
|
- Direct Anthropic API mode (no Claude Code session required) — needs `ANTHROPIC_API_KEY`
|
||||||
|
|||||||
@@ -0,0 +1,332 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>screenshot-rename · before / after</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,400;0,9..144,500;0,9..144,600;1,9..144,400;1,9..144,500;1,9..144,700&family=JetBrains+Mono:wght@400;500;600&display=block">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--paper: #f7f3ed;
|
||||||
|
--paper-2: #efe7d8;
|
||||||
|
--paper-3: #e8dfcd;
|
||||||
|
--ink: #1c1916;
|
||||||
|
--ink-soft: #3c3530;
|
||||||
|
--ink-mute: #8d8478;
|
||||||
|
--accent: #7a1f3d;
|
||||||
|
--accent-soft: rgba(122,31,61,.10);
|
||||||
|
--accent-deep: #5d1730;
|
||||||
|
--rule: #d8cebf;
|
||||||
|
--rule-soft: #e6dfd2;
|
||||||
|
--serif: "Fraunces", Georgia, serif;
|
||||||
|
--mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
width: 1600px;
|
||||||
|
height: 1100px;
|
||||||
|
background: var(--paper);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: var(--serif);
|
||||||
|
font-feature-settings: "ss01", "ss02", "kern";
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(1100px 380px at 92% -10%, rgba(122,31,61,.06), transparent 60%),
|
||||||
|
radial-gradient(900px 320px at 0% 22%, rgba(0,0,0,.03), transparent 70%);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* paper grain */
|
||||||
|
body::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
|
||||||
|
pointer-events: none;
|
||||||
|
mix-blend-mode: multiply;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 70px 88px 64px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── Header ────────────────────────────────────── */
|
||||||
|
.head {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
align-items: end;
|
||||||
|
gap: 36px;
|
||||||
|
padding-bottom: 28px;
|
||||||
|
border-bottom: 1px solid var(--rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent);
|
||||||
|
display: flex; align-items: center; gap: 14px;
|
||||||
|
}
|
||||||
|
.eyebrow .bar { display: inline-block; width: 44px; height: 1px; background: var(--accent); }
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: var(--serif);
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
font-variation-settings: "opsz" 144;
|
||||||
|
font-size: 76px;
|
||||||
|
line-height: 0.96;
|
||||||
|
letter-spacing: -0.022em;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.title .ampersand { font-weight: 300; color: var(--accent); padding: 0 4px; }
|
||||||
|
|
||||||
|
.runtag {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
color: var(--ink-mute);
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.runtag .v { color: var(--ink); display: block; font-size: 13px; margin-top: 4px; letter-spacing: 0.06em; }
|
||||||
|
|
||||||
|
/* ───── Column legend ─────────────────────────────── */
|
||||||
|
.legend {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 30px minmax(0, 1fr) minmax(0, 1.55fr);
|
||||||
|
gap: 32px;
|
||||||
|
margin-top: 32px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-mute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── Rows ──────────────────────────────────────── */
|
||||||
|
.rows { margin-top: 14px; }
|
||||||
|
.row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 30px minmax(0, 1fr) minmax(0, 1.55fr);
|
||||||
|
gap: 32px;
|
||||||
|
padding: 18px 0 18px;
|
||||||
|
border-top: 1px dashed var(--rule);
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.row:first-child { border-top: 1px solid var(--rule); }
|
||||||
|
|
||||||
|
.glyph {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--accent);
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.before {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 17px;
|
||||||
|
color: var(--ink-mute);
|
||||||
|
letter-spacing: -0.005em;
|
||||||
|
}
|
||||||
|
.before .nbsp {
|
||||||
|
display: inline-block;
|
||||||
|
background: rgba(122,31,61,.16);
|
||||||
|
border-bottom: 1px dashed var(--accent);
|
||||||
|
width: 0.4em;
|
||||||
|
height: 1.05em;
|
||||||
|
vertical-align: -0.18em;
|
||||||
|
margin: 0 -0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.after {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 17.5px;
|
||||||
|
color: var(--ink);
|
||||||
|
letter-spacing: -0.005em;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.after .desc {
|
||||||
|
color: var(--accent-deep);
|
||||||
|
font-weight: 500;
|
||||||
|
background: linear-gradient(180deg, transparent 56%, var(--accent-soft) 56%, var(--accent-soft) 100%);
|
||||||
|
padding: 0 1px;
|
||||||
|
}
|
||||||
|
.after .keep {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* annotation callout to the right of certain rows */
|
||||||
|
.row .note {
|
||||||
|
position: absolute;
|
||||||
|
right: -76px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent);
|
||||||
|
white-space: nowrap;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.row.callout .note {
|
||||||
|
display: block;
|
||||||
|
position: static;
|
||||||
|
transform: none;
|
||||||
|
grid-column: 2 / 4;
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
color: var(--ink-mute);
|
||||||
|
}
|
||||||
|
.row.callout .note::before {
|
||||||
|
content: "↳";
|
||||||
|
color: var(--accent);
|
||||||
|
font-family: var(--serif);
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
.row.callout .note b {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── Footer strip ─────────────────────────────── */
|
||||||
|
.foot {
|
||||||
|
margin-top: 48px;
|
||||||
|
padding-top: 26px;
|
||||||
|
border-top: 1px solid var(--rule);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 36px;
|
||||||
|
}
|
||||||
|
.foot .stat {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-mute);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.foot .stat b {
|
||||||
|
display: block;
|
||||||
|
font-family: var(--serif);
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
font-variation-settings: "opsz" 96;
|
||||||
|
font-size: 38px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
text-transform: none;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-top: 6px;
|
||||||
|
line-height: 1.05;
|
||||||
|
}
|
||||||
|
.foot .stat b .small { font-style: normal; font-weight: 500; font-size: 16px; color: var(--ink); }
|
||||||
|
|
||||||
|
.colophon {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 28px; right: 88px;
|
||||||
|
font-family: var(--serif);
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--ink-mute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sheet number */
|
||||||
|
.sheet {
|
||||||
|
position: absolute;
|
||||||
|
top: 70px; right: 88px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
color: var(--ink-mute);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.sheet b { color: var(--accent); }
|
||||||
|
|
||||||
|
::selection { background: var(--accent); color: var(--paper); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="frame">
|
||||||
|
|
||||||
|
<div class="sheet">Sheet · <b>01 / 02</b></div>
|
||||||
|
|
||||||
|
<div class="head">
|
||||||
|
<div class="eyebrow"><span class="bar"></span>screenshot-rename · plate i</div>
|
||||||
|
<h1 class="title">Before <span class="ampersand">&</span> after</h1>
|
||||||
|
<div class="runtag">A real run, twenty files<span class="v">2026·05·04</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<span></span>
|
||||||
|
<span>Filename · before</span>
|
||||||
|
<span>Filename · after</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rows">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<span class="glyph">▸</span>
|
||||||
|
<span class="before">CleanShot 2026-04-15 at 09.14.07.png</span>
|
||||||
|
<span class="after">CleanShot - <span class="desc">Shamel Studio Affiliate Referral Code Modal</span> - 2026-04-15 at 09.14.07.png</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row callout">
|
||||||
|
<span class="glyph">▸</span>
|
||||||
|
<span class="before">Screenshot 2026-03-12 at 11.42.18<span class="nbsp" title="U+202F"></span>PM.png</span>
|
||||||
|
<span class="after">Screenshot - <span class="desc">Synqora Modal Architecture Decisions Diagram</span> - 2026-03-12 at 11.42.18 PM.png</span>
|
||||||
|
<span class="note"><b>U+202F</b> · Apple's narrow no-break space, normalized in plan + execute</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row callout">
|
||||||
|
<span class="glyph">▸</span>
|
||||||
|
<span class="before"><span class="keep" style="color: var(--accent); font-weight: 600;">jojo travel</span> CleanShot 2026-03-31 at 10.52.34.png</span>
|
||||||
|
<span class="after">CleanShot - <span class="desc"><span class="keep">Jojo Travel</span> Flight Australia Melbourne Flightaware Map Route</span> - 2026-03-31 at 10.52.34.png</span>
|
||||||
|
<span class="note"><b>User keyword</b> · hand-typed prefix is preserved and title-cased into the new name</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<span class="glyph">▸</span>
|
||||||
|
<span class="before">CleanShot 2026-01-26 at 17.38.30.png</span>
|
||||||
|
<span class="after">CleanShot - <span class="desc">Claude Code Hitting Limits Conversation Transcript</span> - 2026-01-26 at 17.38.30.png</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<span class="glyph">▸</span>
|
||||||
|
<span class="before">CleanShot 2026-02-09 at 07.29.35.png</span>
|
||||||
|
<span class="after">CleanShot - <span class="desc">Claude Code Skill Rust Refactor Diff View</span> - 2026-02-09 at 07.29.35.png</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="foot">
|
||||||
|
<div class="stat">Files in run<b>20<span class="small"> files</span></b></div>
|
||||||
|
<div class="stat">Renamed<b>20<span class="small"> ✓</span></b></div>
|
||||||
|
<div class="stat">Prefix variants<b>3<span class="small"> CleanShot · Screenshot · keyword</span></b></div>
|
||||||
|
<div class="stat">Idempotent rerun<b>safe<span class="small"> already-renamed skipped</span></b></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="colophon">Set in Fraunces & JetBrains Mono. Plate i of ii.</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 654 KiB |
@@ -0,0 +1,340 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>screenshot-rename · session</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,400;0,9..144,500;0,9..144,600;1,9..144,400;1,9..144,500;1,9..144,700&family=JetBrains+Mono:wght@400;500;600&display=block">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--paper: #f7f3ed;
|
||||||
|
--paper-2: #efe7d8;
|
||||||
|
--paper-3: #e8dfcd;
|
||||||
|
--ink: #1c1916;
|
||||||
|
--ink-2: #221e1a;
|
||||||
|
--ink-soft: #3c3530;
|
||||||
|
--ink-mute: #8d8478;
|
||||||
|
--accent: #7a1f3d;
|
||||||
|
--accent-soft: rgba(122,31,61,.10);
|
||||||
|
--accent-deep: #5d1730;
|
||||||
|
--rule: #d8cebf;
|
||||||
|
--rule-soft: #e6dfd2;
|
||||||
|
--term-paper: #ece4d2;
|
||||||
|
--term-mute: #a8a094;
|
||||||
|
--serif: "Fraunces", Georgia, serif;
|
||||||
|
--mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
width: 1600px;
|
||||||
|
height: 1100px;
|
||||||
|
background: var(--paper);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: var(--serif);
|
||||||
|
font-feature-settings: "ss01", "ss02", "kern";
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(1100px 380px at 92% -10%, rgba(122,31,61,.06), transparent 60%),
|
||||||
|
radial-gradient(900px 320px at 0% 22%, rgba(0,0,0,.03), transparent 70%);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
body::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
|
||||||
|
pointer-events: none;
|
||||||
|
mix-blend-mode: multiply;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 70px 88px 64px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── Header ────────────────────────────────────── */
|
||||||
|
.head {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
align-items: end;
|
||||||
|
gap: 36px;
|
||||||
|
padding-bottom: 28px;
|
||||||
|
border-bottom: 1px solid var(--rule);
|
||||||
|
}
|
||||||
|
.eyebrow {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent);
|
||||||
|
display: flex; align-items: center; gap: 14px;
|
||||||
|
}
|
||||||
|
.eyebrow .bar { display: inline-block; width: 44px; height: 1px; background: var(--accent); }
|
||||||
|
.title {
|
||||||
|
font-family: var(--serif);
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
font-variation-settings: "opsz" 144;
|
||||||
|
font-size: 76px;
|
||||||
|
line-height: 0.96;
|
||||||
|
letter-spacing: -0.022em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.title .ampersand { color: var(--accent); padding: 0 4px; font-weight: 300; }
|
||||||
|
.runtag {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
color: var(--ink-mute);
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.runtag .v { color: var(--ink); display: block; font-size: 13px; margin-top: 4px; letter-spacing: 0.06em; }
|
||||||
|
.sheet {
|
||||||
|
position: absolute;
|
||||||
|
top: 70px; right: 88px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
color: var(--ink-mute);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.sheet b { color: var(--accent); }
|
||||||
|
|
||||||
|
/* ───── Terminal card ─────────────────────────────── */
|
||||||
|
.terminal {
|
||||||
|
margin-top: 38px;
|
||||||
|
background: var(--ink);
|
||||||
|
color: var(--term-paper);
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
padding: 30px 38px 28px;
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80px 1fr;
|
||||||
|
gap: 22px 24px;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
.terminal::after {
|
||||||
|
content: "claude code · session 0a3f";
|
||||||
|
position: absolute;
|
||||||
|
top: 10px; right: 18px;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-mute);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-mute);
|
||||||
|
text-align: right;
|
||||||
|
padding-top: 3px;
|
||||||
|
}
|
||||||
|
.tag.you { color: var(--accent); }
|
||||||
|
.tag.bash { color: #c8d3a3; }
|
||||||
|
.tag.cc { color: #d8a4b3; }
|
||||||
|
.tag.fan { color: var(--accent); }
|
||||||
|
|
||||||
|
.line { font-size: 15px; }
|
||||||
|
.line.you { color: var(--term-paper); font-style: italic; font-family: var(--serif); font-size: 18px; line-height: 1.45; font-weight: 400; }
|
||||||
|
.line.cc { color: var(--term-paper); font-size: 15px; line-height: 1.55; }
|
||||||
|
.line.cc em { color: #d8a4b3; font-style: normal; font-weight: 500; }
|
||||||
|
.line.bash { color: var(--term-mute); font-size: 13px; line-height: 1.65; }
|
||||||
|
.line.bash b { color: var(--term-paper); font-weight: 500; }
|
||||||
|
.line.bash .num { color: #e9c98c; }
|
||||||
|
.line.bash .com { color: #6f685d; font-style: italic; }
|
||||||
|
|
||||||
|
/* ───── Agent fan ─────────────────────────────────── */
|
||||||
|
.fan-wrap { padding: 4px 0 6px; }
|
||||||
|
.agents {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.agent {
|
||||||
|
background: var(--ink-2);
|
||||||
|
border: 1px solid #2c2722;
|
||||||
|
padding: 14px 16px 13px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 22px 1fr auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.agent .dot {
|
||||||
|
width: 10px; height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
box-shadow: 0 0 0 3px rgba(122,31,61,.18);
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
.agent .id {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--term-paper);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.agent .id b { color: #d8a4b3; font-weight: 500; }
|
||||||
|
.agent .ct {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--term-mute);
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
.agent .ct .ok { color: var(--accent); font-weight: 600; padding-left: 2px; }
|
||||||
|
.agent .haiku-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px; left: 12px;
|
||||||
|
background: var(--ink);
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 0 6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── Receipt strip beneath ─────────────────────── */
|
||||||
|
.receipt {
|
||||||
|
margin-top: 38px;
|
||||||
|
background: var(--paper-2);
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
padding: 28px 36px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
gap: 36px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.receipt::before, .receipt::after {
|
||||||
|
content: ""; position: absolute; left: 0; right: 0; height: 8px;
|
||||||
|
background-image: radial-gradient(circle at 6px 4px, var(--paper) 4px, transparent 4px);
|
||||||
|
background-size: 14px 8px;
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
}
|
||||||
|
.receipt::before { top: -1px; }
|
||||||
|
.receipt::after { bottom: -1px; transform: scaleY(-1); }
|
||||||
|
|
||||||
|
.receipt .cell .k {
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-mute);
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.receipt .cell .v {
|
||||||
|
font-family: var(--serif);
|
||||||
|
font-style: italic;
|
||||||
|
font-variation-settings: "opsz" 96;
|
||||||
|
font-size: 32px;
|
||||||
|
line-height: 1.05;
|
||||||
|
color: var(--ink);
|
||||||
|
letter-spacing: -0.012em;
|
||||||
|
}
|
||||||
|
.receipt .cell .v .ok { color: var(--accent); font-weight: 500; }
|
||||||
|
.receipt .cell .v .small {
|
||||||
|
font-style: normal;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colophon {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 28px; right: 88px;
|
||||||
|
font-family: var(--serif);
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--ink-mute);
|
||||||
|
}
|
||||||
|
::selection { background: var(--accent); color: var(--paper); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="frame">
|
||||||
|
|
||||||
|
<div class="sheet">Sheet · <b>02 / 02</b></div>
|
||||||
|
|
||||||
|
<div class="head">
|
||||||
|
<div class="eyebrow"><span class="bar"></span>screenshot-rename · plate ii</div>
|
||||||
|
<h1 class="title">A session, <span class="ampersand">end to end</span></h1>
|
||||||
|
<div class="runtag">196 files · 10 agents · ~3 min<span class="v">claude code · 2026·05·04</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="terminal">
|
||||||
|
|
||||||
|
<span class="tag you">you</span>
|
||||||
|
<div class="line you">rename all the cleanshots in <code style="font-family:var(--mono); font-size:15px; color:#e9c98c;">~/Documents/Screenshots/</code> based on their content.</div>
|
||||||
|
|
||||||
|
<span class="tag cc">claude</span>
|
||||||
|
<div class="line cc">I'll prep the folder, batch into ten files of 19, dispatch <em>parallel Haiku agents</em>, then plan and execute. Resizing Retinas first; extracting frames from <em>.mp4</em> and <em>.pdf</em>.</div>
|
||||||
|
|
||||||
|
<span class="tag bash">bash</span>
|
||||||
|
<div class="line bash"><span class="com"># 1 · prep</span><br>
|
||||||
|
python3 pipeline.py prep --src <span style="color:#e9c98c">~/Documents/Screenshots/</span><br>
|
||||||
|
→ <b><span class="num">196</span></b> source files · <b><span class="num">9</span></b> mp4/pdf frames · resized to <b>1568 px</b> · <b><span class="num">10</span></b> batches written
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="tag fan">fan-out</span>
|
||||||
|
<div class="line fan-wrap">
|
||||||
|
<div class="agents">
|
||||||
|
<div class="agent"><span class="haiku-tag">haiku</span><span class="dot"></span><span class="id">batch-<b>01</b></span><span class="ct">19/19 <span class="ok">✓</span></span></div>
|
||||||
|
<div class="agent"><span class="haiku-tag">haiku</span><span class="dot"></span><span class="id">batch-<b>02</b></span><span class="ct">19/19 <span class="ok">✓</span></span></div>
|
||||||
|
<div class="agent"><span class="haiku-tag">haiku</span><span class="dot"></span><span class="id">batch-<b>03</b></span><span class="ct">19/19 <span class="ok">✓</span></span></div>
|
||||||
|
<div class="agent"><span class="haiku-tag">haiku</span><span class="dot"></span><span class="id">batch-<b>04</b></span><span class="ct">19/19 <span class="ok">✓</span></span></div>
|
||||||
|
<div class="agent"><span class="haiku-tag">haiku</span><span class="dot"></span><span class="id">batch-<b>05</b></span><span class="ct">19/19 <span class="ok">✓</span></span></div>
|
||||||
|
<div class="agent"><span class="haiku-tag">haiku</span><span class="dot"></span><span class="id">batch-<b>06</b></span><span class="ct">19/19 <span class="ok">✓</span></span></div>
|
||||||
|
<div class="agent"><span class="haiku-tag">haiku</span><span class="dot"></span><span class="id">batch-<b>07</b></span><span class="ct">19/19 <span class="ok">✓</span></span></div>
|
||||||
|
<div class="agent"><span class="haiku-tag">haiku</span><span class="dot"></span><span class="id">batch-<b>08</b></span><span class="ct">19/19 <span class="ok">✓</span></span></div>
|
||||||
|
<div class="agent"><span class="haiku-tag">haiku</span><span class="dot"></span><span class="id">batch-<b>09</b></span><span class="ct">19/19 <span class="ok">✓</span></span></div>
|
||||||
|
<div class="agent"><span class="haiku-tag">haiku</span><span class="dot"></span><span class="id">batch-<b>10</b></span><span class="ct">10/10 <span class="ok">✓</span></span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="tag bash">bash</span>
|
||||||
|
<div class="line bash"><span class="com"># 2 · plan, then execute</span><br>
|
||||||
|
python3 pipeline.py plan --src <span style="color:#e9c98c">~/Documents/Screenshots/</span> → <b><span class="num">189</span></b> renames · <b><span class="num">0</span></b> errors · <b><span class="num">0</span></b> collisions<br>
|
||||||
|
python3 pipeline.py execute --src <span style="color:#e9c98c">~/Documents/Screenshots/</span> → audit <b><span class="num">195</span></b> = <b><span class="num">195</span></b> · <span style="color:#d8a4b3;">189 ✓</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="receipt">
|
||||||
|
<div class="cell">
|
||||||
|
<span class="k">Agents in flight</span>
|
||||||
|
<span class="v">10<span class="small">parallel</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<span class="k">Renames committed</span>
|
||||||
|
<span class="v"><span class="ok">189</span><span class="small">/ 189 planned</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<span class="k">File-count audit</span>
|
||||||
|
<span class="v">195 <span style="color:var(--ink-mute);">=</span> 195 <span class="ok">✓</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="cell">
|
||||||
|
<span class="k">Files lost</span>
|
||||||
|
<span class="v"><span class="ok">0</span><span class="small">zero overwrites</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="colophon">Set in Fraunces & JetBrains Mono. Plate ii of ii.</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 455 KiB |
+243
-6
@@ -657,6 +657,132 @@ footer .colophon {
|
|||||||
max-width: 50ch;
|
max-width: 50ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ───── Plate (image + caption) ───────────────── */
|
||||||
|
|
||||||
|
.plate {
|
||||||
|
margin: clamp(56px, 7vw, 96px) 0 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.plate figure {
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.plate figure::before, .plate figure::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 56px; height: 1px;
|
||||||
|
background: var(--accent);
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
.plate figure::before { left: -68px; }
|
||||||
|
.plate figure::after { right: -68px; }
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.plate figure::before, .plate figure::after { display: none; }
|
||||||
|
}
|
||||||
|
.plate img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1180px;
|
||||||
|
height: auto;
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
background: var(--paper);
|
||||||
|
box-shadow:
|
||||||
|
0 28px 56px -34px rgba(28,25,22,.30),
|
||||||
|
0 6px 14px -8px rgba(28,25,22,.18);
|
||||||
|
}
|
||||||
|
.plate figcaption {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-mute);
|
||||||
|
margin-top: 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.plate figcaption .dot { color: var(--accent); }
|
||||||
|
.plate figcaption .b { color: var(--ink); }
|
||||||
|
|
||||||
|
/* ───── Additions (what's new this revision) ──── */
|
||||||
|
|
||||||
|
.additions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0;
|
||||||
|
border: 1px solid var(--rule);
|
||||||
|
background: var(--paper-2);
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
@media (max-width: 920px) { .additions { grid-template-columns: 1fr; } }
|
||||||
|
.add {
|
||||||
|
padding: 32px 28px 28px;
|
||||||
|
border-right: 1px solid var(--rule);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.add:last-child { border-right: none; }
|
||||||
|
@media (max-width: 920px) {
|
||||||
|
.add { border-right: none; border-bottom: 1px solid var(--rule); }
|
||||||
|
.add:last-child { border-bottom: none; }
|
||||||
|
}
|
||||||
|
.add .badge {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.20em;
|
||||||
|
color: var(--accent);
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.add .badge .num {
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 22px; height: 22px;
|
||||||
|
text-align: center; line-height: 20px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.add h3 {
|
||||||
|
font-family: var(--serif);
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 500;
|
||||||
|
font-variation-settings: "opsz" 60;
|
||||||
|
font-size: 26px;
|
||||||
|
line-height: 1.15;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
letter-spacing: -0.012em;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.add p {
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
line-height: 1.55;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
}
|
||||||
|
.add code {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--accent-deep);
|
||||||
|
background: rgba(122,31,61,.08);
|
||||||
|
padding: 1px 6px;
|
||||||
|
}
|
||||||
|
.add .glyph {
|
||||||
|
position: absolute;
|
||||||
|
top: 24px; right: 22px;
|
||||||
|
font-family: var(--serif);
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 42px;
|
||||||
|
color: var(--accent-soft);
|
||||||
|
line-height: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* ───── Reveal animation ─────────────────────── */
|
/* ───── Reveal animation ─────────────────────── */
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
@@ -686,8 +812,9 @@ footer .colophon {
|
|||||||
<nav class="top">
|
<nav class="top">
|
||||||
<a href="#problem">problem</a>
|
<a href="#problem">problem</a>
|
||||||
<a href="#pipeline">pipeline</a>
|
<a href="#pipeline">pipeline</a>
|
||||||
|
<a href="#whats-new">what's new</a>
|
||||||
<a href="#gotchas">gotchas</a>
|
<a href="#gotchas">gotchas</a>
|
||||||
<a href="#cases">use cases</a>
|
<a href="#session">session</a>
|
||||||
<a href="#install">install</a>
|
<a href="#install">install</a>
|
||||||
<a href="https://gitea.tojo.team/cardinale/screenshot-rename">repo ↗</a>
|
<a href="https://gitea.tojo.team/cardinale/screenshot-rename">repo ↗</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -837,14 +964,29 @@ footer .colophon {
|
|||||||
<p style="font-family:var(--mono); font-size:12px; color:var(--ink-mute); letter-spacing:0.1em; text-transform:uppercase; margin-top:20px;">
|
<p style="font-family:var(--mono); font-size:12px; color:var(--ink-mute); letter-spacing:0.1em; text-transform:uppercase; margin-top:20px;">
|
||||||
The original timestamp survives unchanged. Sorting still works. The description sits between, set off by em-dashes.
|
The original timestamp survives unchanged. Sorting still works. The description sits between, set off by em-dashes.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div class="plate reveal">
|
||||||
|
<figure>
|
||||||
|
<img src="https://gitea.tojo.team/cardinale/screenshot-rename/raw/branch/main/assets/before-after.png" alt="Plate i — five real renames, including U+202F handling and user-keyword preservation">
|
||||||
|
<figcaption>
|
||||||
|
<span class="bar" style="display:inline-block;width:28px;height:1px;background:var(--ink-mute);"></span>
|
||||||
|
<span>Plate i</span>
|
||||||
|
<span class="dot">·</span>
|
||||||
|
<span class="b">Five real renames</span>
|
||||||
|
<span class="dot">·</span>
|
||||||
|
<span>CleanShot · Apple Screenshot · user-keyword</span>
|
||||||
|
<span class="bar" style="display:inline-block;width:28px;height:1px;background:var(--ink-mute);"></span>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ───── Receipt ──────────────────────────────────── -->
|
<!-- ───── Receipt (run ii) ─────────────────────────── -->
|
||||||
<section>
|
<section>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div class="receipt reveal">
|
<div class="receipt reveal">
|
||||||
<div class="head">screenshot-rename · run log · 2026-05-04</div>
|
<div class="head">screenshot-rename · run ii · 2026-05-04 · 196 files</div>
|
||||||
<div class="line"><span>source files</span><span class="v">196</span></div>
|
<div class="line"><span>source files</span><span class="v">196</span></div>
|
||||||
<div class="line"><span>resized to 1568px</span><span class="v">196</span></div>
|
<div class="line"><span>resized to 1568px</span><span class="v">196</span></div>
|
||||||
<div class="line"><span>frames extracted (mp4 / pdf)</span><span class="v">9</span></div>
|
<div class="line"><span>frames extracted (mp4 / pdf)</span><span class="v">9</span></div>
|
||||||
@@ -860,10 +1002,74 @@ footer .colophon {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- ───── What's new ───────────────────────────────── -->
|
||||||
|
<section id="whats-new">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="section-label"><span class="num">04</span>What's new this revision</div>
|
||||||
|
<h2><em>Three</em> additions, one mixed run.</h2>
|
||||||
|
<p class="lede-2">
|
||||||
|
A second batch of files surfaced cases the first run never hit: Apple's own
|
||||||
|
<em>Screenshot</em> filenames, user-typed keyword prefixes, and folders where some
|
||||||
|
files were already renamed. The pipeline now handles all three in a single pass
|
||||||
|
— and the parser learned a new gotcha along the way.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="additions reveal">
|
||||||
|
<div class="add">
|
||||||
|
<span class="glyph">a.</span>
|
||||||
|
<div class="badge"><span class="num">i</span>multi-prefix</div>
|
||||||
|
<h3>CleanShot <em>and</em> Screenshot, in one run.</h3>
|
||||||
|
<p>
|
||||||
|
The parser now accepts both <code>CleanShot 2026-MM-DD at HH.MM.SS.png</code> and
|
||||||
|
Apple's <code>Screenshot 2026-MM-DD at H.MM.SS PM.png</code>. Mixed folders no
|
||||||
|
longer need two passes — the manifest builder picks up either prefix.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="add">
|
||||||
|
<span class="glyph">b.</span>
|
||||||
|
<div class="badge"><span class="num">ii</span>keyword preservation</div>
|
||||||
|
<h3>Hand-typed prefixes survive.</h3>
|
||||||
|
<p>
|
||||||
|
A file named <code>jojo travel CleanShot 2026-...png</code> carries user knowledge
|
||||||
|
the AI doesn't have. The parser strips the keyword phrase, title-cases it, and
|
||||||
|
prepends it to the AI description — so the new filename reads
|
||||||
|
<em>Jojo Travel Flight Australia Melbourne Flightaware Map Route</em>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="add">
|
||||||
|
<span class="glyph">c.</span>
|
||||||
|
<div class="badge"><span class="num">iii</span>idempotent re-runs</div>
|
||||||
|
<h3>Re-running won't stack descriptions.</h3>
|
||||||
|
<p>
|
||||||
|
The parser now detects names already in the
|
||||||
|
<code>App - Description - timestamp.ext</code> form and excludes them from the
|
||||||
|
manifest. You can re-run the skill on a partially-renamed folder without the
|
||||||
|
name growing on every pass.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="receipt reveal" style="max-width: 820px;">
|
||||||
|
<div class="head">screenshot-rename · run iii · 2026-05-04 · mixed prefixes</div>
|
||||||
|
<div class="line"><span>source files</span><span class="v">20</span></div>
|
||||||
|
<div class="line"><span>CleanShot</span><span class="v">14</span></div>
|
||||||
|
<div class="line"><span>Apple Screenshot (with U+202F)</span><span class="v">5</span></div>
|
||||||
|
<div class="line"><span>user-prefixed</span><span class="v">1</span></div>
|
||||||
|
<div class="line"><span>already-renamed (skipped)</span><span class="v">0</span></div>
|
||||||
|
<div class="line"><span>plan validated</span><span class="v">20 renames · 0 errors</span></div>
|
||||||
|
<div class="line"><span>file count before / after</span><span class="v">20 = 20</span></div>
|
||||||
|
<div class="line total"><span>renames committed</span><span class="ok">20 ✓</span></div>
|
||||||
|
<div class="line total"><span>files lost</span><span class="ok">0 ✓</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- ───── Gotchas ──────────────────────────────────── -->
|
<!-- ───── Gotchas ──────────────────────────────────── -->
|
||||||
<section id="gotchas">
|
<section id="gotchas">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div class="section-label"><span class="num">04</span>The rules that prevent data loss</div>
|
<div class="section-label"><span class="num">05</span>The rules that prevent data loss</div>
|
||||||
<h2>Every rule below was <em>paid for</em>.</h2>
|
<h2>Every rule below was <em>paid for</em>.</h2>
|
||||||
<p class="lede-2">
|
<p class="lede-2">
|
||||||
During development, four files were destroyed by a one-line bash mistake.
|
During development, four files were destroyed by a one-line bash mistake.
|
||||||
@@ -880,6 +1086,9 @@ footer .colophon {
|
|||||||
<li><b>Run renames foreground.</b><code>Bash run_in_background</code> with <code>while read</code> may exit early with no progress. Run via Python in the same shell — <code>os.rename</code> is just a syscall.</li>
|
<li><b>Run renames foreground.</b><code>Bash run_in_background</code> with <code>while read</code> may exit early with no progress. Run via Python in the same shell — <code>os.rename</code> is just a syscall.</li>
|
||||||
<li><b>Validate the filename column.</b>Haiku occasionally returns the resized <code>.jpg</code> name instead of the original <code>.png</code>. The plan-builder must try alternate extensions when the claimed source isn't found.</li>
|
<li><b>Validate the filename column.</b>Haiku occasionally returns the resized <code>.jpg</code> name instead of the original <code>.png</code>. The plan-builder must try alternate extensions when the claimed source isn't found.</li>
|
||||||
<li><b>Preserve the original extension.</b>The pipeline reads from a resized JPEG but renames the original <code>.mp4</code> / <code>.pdf</code>. Write the source extension back into the new name.</li>
|
<li><b>Preserve the original extension.</b>The pipeline reads from a resized JPEG but renames the original <code>.mp4</code> / <code>.pdf</code>. Write the source extension back into the new name.</li>
|
||||||
|
<li><b>Apple Screenshot files use <code>U+202F</code>.</b>The narrow no-break space sits between the seconds and AM/PM. Haiku echoes it as ASCII space, the lookup misses, and every Screenshot file is dropped from the plan with a misleading <code>NO_DESC</code> error. Normalize on both sides; emit ASCII space in the new name.</li>
|
||||||
|
<li><b>Re-runs must skip already-renamed files.</b>Without an <code>^App - .+ - timestamp.ext$</code> exclusion rule the parser will pile a second AI description into every name on every run. The pipeline detects and excludes them.</li>
|
||||||
|
<li><b>User-typed keyword prefix is signal.</b>A name like <code>jojo travel CleanShot ...</code> carries knowledge the AI doesn't have. Strip the keyword phrase, title-case it, and prepend it to the description before assembly. Don't drop it.</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -887,7 +1096,7 @@ footer .colophon {
|
|||||||
<!-- ───── Use cases ────────────────────────────────── -->
|
<!-- ───── Use cases ────────────────────────────────── -->
|
||||||
<section id="cases">
|
<section id="cases">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div class="section-label"><span class="num">05</span>Use cases</div>
|
<div class="section-label"><span class="num">06</span>Use cases</div>
|
||||||
<h2>What this looks like in <em>practice</em>.</h2>
|
<h2>What this looks like in <em>practice</em>.</h2>
|
||||||
<p class="lede-2">
|
<p class="lede-2">
|
||||||
The skill earns its keep when "Spotlight will find it" stops being true. Four scenarios where it has.
|
The skill earns its keep when "Spotlight will find it" stops being true. Four scenarios where it has.
|
||||||
@@ -929,10 +1138,38 @@ footer .colophon {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- ───── Session (plate ii) ───────────────────────── -->
|
||||||
|
<section id="session">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="section-label"><span class="num">07</span>A session, end to end</div>
|
||||||
|
<h2>What it <em>looks like</em> when you ask.</h2>
|
||||||
|
<p class="lede-2">
|
||||||
|
The skill is conversational at the top, mechanical underneath. You ask in plain
|
||||||
|
English; ten Haiku agents fan out in a single round-trip; Python validates the
|
||||||
|
plan and applies it under the file-count audit. The whole thing fits on one page.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="plate reveal">
|
||||||
|
<figure>
|
||||||
|
<img src="https://gitea.tojo.team/cardinale/screenshot-rename/raw/branch/main/assets/session.png" alt="Plate ii — Claude Code session showing user prompt, parallel Haiku fan-out across ten batches, and the run receipt">
|
||||||
|
<figcaption>
|
||||||
|
<span class="bar" style="display:inline-block;width:28px;height:1px;background:var(--ink-mute);"></span>
|
||||||
|
<span>Plate ii</span>
|
||||||
|
<span class="dot">·</span>
|
||||||
|
<span class="b">A session, end to end</span>
|
||||||
|
<span class="dot">·</span>
|
||||||
|
<span>10 agents · ~3 min · zero loss</span>
|
||||||
|
<span class="bar" style="display:inline-block;width:28px;height:1px;background:var(--ink-mute);"></span>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- ───── Install ──────────────────────────────────── -->
|
<!-- ───── Install ──────────────────────────────────── -->
|
||||||
<section id="install">
|
<section id="install">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div class="section-label"><span class="num">06</span>Install & run</div>
|
<div class="section-label"><span class="num">08</span>Install & run</div>
|
||||||
<h2>Three commands, <em>one folder</em>.</h2>
|
<h2>Three commands, <em>one folder</em>.</h2>
|
||||||
<p class="lede-2">
|
<p class="lede-2">
|
||||||
The skill installs as a Claude Code skill. Once cloned into <code>~/.claude/skills/</code>, it
|
The skill installs as a Claude Code skill. Once cloned into <code>~/.claude/skills/</code>, it
|
||||||
|
|||||||
Reference in New Issue
Block a user