# InventIcons

Obsidian plugin that adds custom icons and colors next to file and folder
names in the file explorer.

## Features

- Custom icon and color per file or folder (right-click → **InventIcons: customize icon**)
- Separate folder icons for collapsed and expanded states
- Four icon sources: Lucide (built into Obsidian), Tabler, Boxicons, Phosphor
- Emoji picker with categories and English name search
- Optional icon next to the note's inline title
- Text styling per file/folder: font family, size, weight, italic, color
- Global default icons for files and folders
- Regex path masks — assign icons to whole groups of paths (first match wins)
- Icon placement: alongside the name, or replacing the folder collapse arrows
- Adjustable icon sizes
- File explorer styling: background color, horizontal separator lines,
  vertical indent guide lines
- Config stored **inside your vault by default** as a portable JSON file, so it
  syncs between devices with whatever sync you already use (watched for
  external changes); the classic plugin folder stays available as an option

## Free and Pro

The free version is a complete, working plugin — not a trial. It has capacity
ceilings; a one-time Pro license removes them and adds the tiered features.

| | Free | Pro |
| --- | --- | --- |
| Icons on files & folders | up to **100** items | unlimited |
| Lucide + all emoji, with search | ✅ | ✅ |
| Tabler (6 324) · Boxicons (3 768) · Phosphor (9 198) | browse only | ✅ |
| Colour | 14-swatch palette | any CSS colour |
| Collapsed / expanded folder icons | ✅ | ✅ |
| Icon next to the note title | ✅ | ✅ |
| Icon size, placement, storage location | ✅ | ✅ |
| Regex path masks | **1** | unlimited |
| Global default icons | — | ✅ |
| Text styling (font, size, weight, italic, colour) | — | ✅ |
| Explorer styling (background, separators, indent guides) | — | ✅ |

Rules the implementation keeps, deliberately:

- **Rendering is never gated.** A configuration that exceeds the free ceiling —
  hand-edited, synced in from a licensed device, or left after a hold — keeps
  displaying every icon it defines. Ceilings only refuse to create something
  new, so a vault can never end up looking broken.
- **Existing entries stay editable.** At 100/100 you can still change or remove
  what you have; only a 101st *new* item is refused.
- **No nags.** The upgrade notice appears at the moment a ceiling is hit and
  nowhere else — no timers, no startup popups.
- **Activation is offline.** The key is an Ed25519 signature over your email,
  verified locally. The only network call is a once-a-day standing check that
  asks whether the license was revoked; blocked or offline, the plugin keeps
  working on the last known answer.

## Bundled icon sets and their licenses

Tabler, Boxicons and Phosphor artwork ships inside the plugin — **all three
under the MIT license**, which places no restriction on selling the product.
Lucide does not ship: those icons come from Obsidian's own API, so nothing of
Lucide is redistributed. Full attribution and the complete license texts are in
[THIRD-PARTY-LICENSES.md](THIRD-PARTY-LICENSES.md), which is packaged into every
release. `npm run build:bin` refuses to package if that file is missing or stale.

> **Remix Icon was removed on purpose — do not add it back.** Remix Design
> relicensed the library in January 2026 under the custom *Remix Icon License
> v1.0*, whose §3.1 forbids distributing the icons where they are "the primary
> advertised feature" or where "removing the Icons would eliminate the core
> value of the product". That describes this plugin exactly.
>
> The lesson generalises: **check the upstream LICENSE file, not just the
> package metadata**, before adding or upgrading any icon set. Iconify's
> metadata and npm's `license` field both still said Apache-2.0 months after
> the relicense.

Regenerate the attribution after any icon-set dependency change:

```bash
npm run licenses
```

## Installation

Manual install from the release zip:

1. Download `inventicons-<version>.zip` from the latest release.
2. Extract it — the archive contains a top-level `inventicons/` folder.
3. Copy that folder into `<your vault>/.obsidian/plugins/`, so you end up with
   `.obsidian/plugins/inventicons/main.js`.
4. Reload Obsidian and enable **InventIcons** in Settings → Community plugins.

## Usage

- **Per file/folder:** right-click an item in the file explorer →
  *InventIcons: customize icon*. Pick an icon (Icons or Emoji tab), a color,
  text style, and for folders a separate expanded state. Files can also show
  their icon in the note title.
- **Defaults & masks:** Settings → InventIcons → *Default icons & regex masks*.
  Masks are regular expressions matched against the full path, e.g.
  `^projects/.*\.md$`. Priority: specific entry → first matching mask →
  global default, resolved independently for every property.
- **Storage:** Settings → InventIcons → *Storage location*. The default,
  "inside vault", keeps everything in a JSON file of your choice (default
  `_config/inventicons.json`) so it syncs between devices with your notes; the
  plugin watches the file and reloads it on external changes. Switch to
  "plugin folder" for the classic per-device behaviour.
- **Activation:** Settings → InventIcons → *Activation*. Enter the email you
  bought with and the `II1.` key from the purchase email. The license is
  personal, lifetime and covers all your vaults and devices; it is stored in
  the plugin's own `data.json` and is deliberately **never** written into the
  vault config file, so it cannot leak through a shared or published vault.
- Color text fields accept any CSS color (`#rrggbb`, `red`, `rgb(…)`,
  `hsl(…)`); typing `default` or clearing the field restores the default.

## Development

Install dependencies:

```bash
npm install
```

Build the plugin in place:

```bash
npm run build
```

This compiles `main.ts` into `main.js` in the plugin root. Obsidian loads this
root `main.js` during local development.

The icon sets (`icon-sets/*.json`) and emoji keywords (`emoji-names.json`)
are committed, so a fresh clone builds immediately. To regenerate them from
the upstream packages:

```bash
npm run generate-icons
```

Run the tests (license scheme + tier ceilings):

```bash
npm test
```

## Licensing (vendor side)

Keys are Ed25519 signatures over the buyer's normalized email. The private key
signs, the plugin only ever verifies against the public reference embedded in
`licensing.ts` (`REF`) — nobody can mint a key without the private half.

```bash
# once — creates scripts/license/.private-key (gitignored, mode 600) and prints
# the public key to paste into licensing.ts as REF. BACK THE FILE UP: losing it
# means every future key must be re-issued under a new pair.
node scripts/license/keygen.mjs

# mint a key by hand (support, comps, a buyer whose email bounced)
node scripts/license/sign.mjs buyer@example.com --register
```

`--register` records the key in the D1 registry. Without it, the buyer's first
daily standing check raises an "UNREGISTERED key in use" alert, because the
registry cannot tell a hand-issued key from a forged one.

The webhook + standing endpoint lives in `scripts/license/worker/`:

```bash
cd scripts/license/worker
./setup.sh      # KV namespace + secrets (needs ../.env.webhook and ../.private-key)
./setup-d1.sh   # creates the inventicons_* tables in the shared "solidinvent" D1
./deploy.sh     # deploy; paste the workers.dev URL into Paddle → Notifications
```

Blocking a leaked key (the plugin picks it up on its next daily check, and
falls back to the free tier without deleting anything):

```bash
npx wrangler@4 d1 execute solidinvent --remote --command \
  "UPDATE inventicons_licenses SET active=0, revoked_reason='key leaked', revoked_at=datetime('now') WHERE email='x@y.com'"
```

## Build to `bin`

Create a clean runtime copy in `bin/inventicons`:

```bash
npm run build:bin
```

The output contains:

```text
bin/inventicons/main.js
bin/inventicons/manifest.json
bin/inventicons/styles.css
bin/inventicons/versions.json
bin/inventicons/INSTALL.md
bin/inventicons/README.md
bin/inventicons/LICENSE
bin/inventicons/THIRD-PARTY-LICENSES.md
bin/inventicons/icon-sets/
```

Local user data such as `data.json` is not copied.

In VS Code, run the default build task with:

```text
Cmd+Shift+B
```

This runs the same `build:bin` script through `.vscode/tasks.json`.

## Package zip

Build and create a release zip:

```bash
npm run package
```

This creates and verifies two byte-identical archives:

```text
bin/inventicons-<version>.zip
bin/inventicons.zip
```

For example:

```text
bin/inventicons-1.0.0.zip
```

The zip version is read from `manifest.json`.
The archive contains a top-level `inventicons/` folder, so the plugin files are not loose at the zip root.
`inventicons.zip` is the stable filename served by the website; the numbered
copy is kept as the immutable release artifact. Packaging fails if required
runtime, installation, version or licence files are missing, or if user
`data.json` accidentally enters the archive.

Note: the packaging script uses the system `zip` command, so it requires
macOS or Linux (or WSL/Git Bash on Windows).

## Releasing and versioning

`package.json` is the version source of truth. The bump script keeps all four
release metadata locations synchronized:

```text
manifest.json      → "version": "x.y.z"
package.json       → "version": "x.y.z"
package-lock.json  → root package version
versions.json      → "x.y.z": "<minAppVersion>"
```

The normal release command tests first, increments the patch version exactly
once, builds and verifies both archives, copies the plugin to available local
devices and finally publishes the ZIP plus `version.json` to Cloudflare Pages:

```bash
npm run deploy                    # patch bump: 1.0.0 -> 1.0.1
npm run full-deploy -- minor      # minor bump
npm run full-deploy -- major      # major bump
npm run full-deploy -- --no-bump  # redeploy the current release
npm run deploy:test               # tests + package only; no bump/web/devices
```

Ordinary `build`, `package` and website-only `deploy:web` commands never bump
the plugin version. Versions describe releases, not local builds or a legal
text-only website update. Manual version commands are also available:

```bash
npm run version:patch
npm run version:minor
npm run version:major
```

After a successful full deploy, commit the synchronized files, tag with the
bare version (`git tag x.y.z`) and push the tag.

Pushing a tag runs the GitHub Actions release workflow
(`.github/workflows/release.yml`), which builds the plugin and creates a
GitHub release with `inventicons-<version>.zip` plus the standalone `main.js`,
`manifest.json` and `styles.css` assets.

The download area reads `download/version.json` produced during the deploy and
shows the exact plugin version and release date next to the button.

## License

[MIT](LICENSE)
