160 lines
7.5 KiB
Markdown
160 lines
7.5 KiB
Markdown
<!--
|
|
***************************************************************************
|
|
* (C)opyright 2026 by Ruben Carlo Benante *
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation, either version 3 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* This program is distributed in the hope that it will be useful, *
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
* GNU General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see http://www.gnu.org/licenses/. *
|
|
* *
|
|
* Contact author at: *
|
|
* Ruben Carlo Benante *
|
|
* rcb@beco.cc *
|
|
***************************************************************************
|
|
-->
|
|
|
|
# Zettelclean
|
|
|
|
Keep a Zettelkasten filename's slug in sync with the note's H1 heading.
|
|
|
|
Zettelclean resolves the "three sources of truth for a title" problem (filename vs H1 vs
|
|
YAML `title:`) by separating concerns:
|
|
|
|
- The **H1 heading** in the note body is the single source of truth for the human title.
|
|
- The **filename** is a derived machine identity: `TIMEID-slug(H1).md`.
|
|
- No YAML is written. Files stay pure Markdown.
|
|
|
|
It is a small, one-way, prefix-gated renamer: edit the H1, and a moment later the filename's
|
|
slug tail follows. Nothing else is touched.
|
|
|
|
## How it works
|
|
|
|
- A note is a zettel when its filename starts with a recognized **prefix**: a **TIMEID** (a run
|
|
of 12 to 18 digits) always, and a **`yyyy-mm-dd` date** too if you enable date mode in settings.
|
|
The prefix is followed by any non-digit or the end of the name - so `20260728145404-my-note.md`,
|
|
`202607281454.md`, `202607281454 my note.md`, and (date mode) `2026-07-30 Journal.md` all
|
|
qualify. Files without a recognized prefix are ignored, so plain notes, Excalidraw and Kanban
|
|
files are never renamed.
|
|
- The prefix is fixed and never changes; only the slug tail is regenerated.
|
|
- When you edit the H1, zettelclean waits about 4 seconds of idle, then renames the file to
|
|
`prefix-slug(H1).md` using Obsidian's own rename (so backlinks are updated). A 6 second
|
|
cooldown prevents a just-renamed file from being renamed again.
|
|
- Sync is strictly one-way, H1 to filename. Renaming a file by hand is never fought.
|
|
- **Sticky tail:** the tail follows the H1 when it produces a slug; with no H1 the existing tail
|
|
is kept and cleaned. The tail is never deleted - delete the H1 and the filename keeps its last
|
|
slug. A note that never had a title stays bare (`TIMEID.md`); one with no usable title (empty
|
|
H1 and no sluggable tail) is left untouched.
|
|
- If a rename would collide with an existing filename, zettelclean keeps the current name and
|
|
shows a notice.
|
|
|
|
The slug is case-preserving, ASCII, dash-separated: accents are folded (`Ciências` becomes
|
|
`Ciencias`), runs of punctuation and whitespace collapse to a single dash, and leading and
|
|
trailing dashes are trimmed.
|
|
|
|
## Creating notes
|
|
|
|
Use Obsidian's built-in **Unique note creator** core plugin to mint the TIMEID:
|
|
|
|
1. Settings -> Core plugins -> enable "Unique note creator".
|
|
2. Set "Unique note format" to `YYYYMMDDHHmmss` (14 digits, second precision - avoids
|
|
same-minute collisions).
|
|
3. Click the "Create new unique note" ribbon icon. You get `20260728145404.md`; type your
|
|
`# Title`, and zettelclean takes over from there.
|
|
|
|
Zettelclean also accepts an existing 12-digit (`YYYYMMDDHHmm`) vault if you prefer not to
|
|
change the setting.
|
|
|
|
### Retrofitting existing notes
|
|
|
|
Notes created before you adopted this workflow have no TIMEID. Right-click such a note in the
|
|
file explorer and choose **"Prefix timestamp to filename"**. It mints a fresh timestamp,
|
|
prepends it, and the note is now a zettel that syncs on future H1 edits.
|
|
|
|
### Date-prefixed notes (journaling)
|
|
|
|
Turn on **date mode** in settings to also treat `yyyy-mm-dd`-prefixed notes as zettels. A daily
|
|
note `2026-07-30.md` gains a slug once you title it (`# Gratitude` -> `2026-07-30-Gratitude.md`),
|
|
and `2026-07-30 Morning pages.md` is cleaned to `2026-07-30-Morning-pages.md`.
|
|
|
|
## Slugging a folder name
|
|
|
|
Right-click a folder and choose **"Slug folder name"** to ASCII-clean it in place (this folder
|
|
only): `Citações` becomes `Citacoes`. The rename goes through Obsidian, so `[[links]]` into the
|
|
folder are updated. It aborts if a sibling of that name already exists, and never touches the
|
|
vault root.
|
|
|
|
## Pretty sidebar (optional)
|
|
|
|
By default the file explorer shows the slugged filename (with dashes). If you want the sidebar,
|
|
tabs and graph to show the pretty H1 instead - with no YAML in your files - install the
|
|
**Front Matter Title** community plugin and set its feature templates to:
|
|
|
|
```
|
|
#heading | _basename
|
|
```
|
|
|
|
That reads the first H1 directly and falls back to the filename when there is none.
|
|
|
|
## Settings
|
|
|
|
One setting: **Recognized filename prefixes** (Settings -> Community plugins -> Zettelclean).
|
|
|
|
- **Timestamp only** (default) - only 12-18 digit TIMEID prefixes.
|
|
- **Timestamp and date** - also `yyyy-mm-dd` prefixes, for journaling.
|
|
|
|
The 4 second settle and 6 second cooldown are fixed.
|
|
|
|
## Development
|
|
|
|
Dependencies are managed with **pnpm** (see "Why pnpm, not npm" below). Node 22+ ships
|
|
`corepack`, so you do not need to install pnpm globally, and the `packageManager` field in
|
|
`package.json` pins the exact version.
|
|
|
|
```bash
|
|
corepack pnpm install # first time, or after a dependency change
|
|
corepack pnpm run dev # compile in watch mode
|
|
corepack pnpm run build # typecheck and produce main.js
|
|
corepack pnpm test # run the slug unit tests (Node's built-in runner)
|
|
corepack pnpm run lint # eslint with the Obsidian ruleset
|
|
```
|
|
|
|
The pure string logic lives in `src/slug.ts` and is unit-tested in isolation; the Obsidian
|
|
wiring lives in `src/main.ts`. Tests run on Node's built-in test runner (`node --test`) - there
|
|
is no vitest or jest dependency.
|
|
|
|
### Why pnpm, not npm
|
|
|
|
**Do not run `npm install` or `npm ci` in this repo.** This project is developed on a **ZFS**
|
|
filesystem, and npm's installer renames many directories in parallel (to hoist and dedupe
|
|
packages). That races ZFS's directory-metadata handling and aborts with:
|
|
|
|
```
|
|
npm error code ENOTEMPTY
|
|
npm error syscall rename
|
|
```
|
|
|
|
pnpm avoids the problem entirely: it hard-links packages from a global content-addressable
|
|
store instead of renaming directories into place, so it installs cleanly on the same disk.
|
|
|
|
Notes for contributors:
|
|
|
|
- Only `npm install` / `npm ci` are affected. Running *scripts* through npm (`npm run build`)
|
|
still works, because that just spawns tsc/esbuild and installs nothing. Still, prefer `pnpm`
|
|
everywhere for consistency.
|
|
- The lockfile is `pnpm-lock.yaml` (committed). There is no `package-lock.json`; do not create
|
|
one.
|
|
|
|
## License
|
|
|
|
GNU General Public License v3.0 or later (`GPL-3.0-or-later`).
|
|
Copyright (C) 2026 Ruben Carlo Benante (Dr. Béco) <rcb@beco.cc>.
|
|
See [LICENSE](LICENSE) for the full text.
|