Skip to main content

Plain-text, pass-style bookmarks

Project description

bm — plain‑text bookmarks

A tiny, stdlib‑only bookmark manager inspired by the Unix philosophy and pass:

  • One text file per bookmark (.bm) with front matter + freeform notes
  • Human‑readable paths with a short hash to avoid collisions
  • Stable IDs derived from the URL (rename‑safe)
  • Greppable store; composable CLI
  • Atomic writes & path‑safety checks
  • JSON / JSONL output for pipelines
  • Netscape HTML import/export for browser interoperability
  • Optional Git sync for history and cross‑device

Works on macOS, Linux, WSL, and Windows (PowerShell). No third‑party dependencies.


Table of contents


Why bm?

Most bookmark tools are databases or browser‑locked. bm chooses text first: plain UTF‑8 files that last decades, are easy to diff, and play well with your editor, shell, and Git. It embraces "do one thing well" and stays small so you can integrate it anywhere.


Install

Requires Python >=3.8. Install from PyPI:

pip install bkmrk

For the latest development version, clone and install locally:

git clone https://github.com/jtabke/bkmrk
cd bkmrk
python -m pip install .

Development workflows can pull in the optional extras declared in pyproject.toml:

python -m pip install -e '.[dev]'

Prefer running straight from the repository? The module entry point works without installation:

python -m bm --help

On Windows (PowerShell):

python -m bm --help

Quickstart

# initialize a new store (optionally a git repo)
bm init --git

# import bookmarks from a browser export (Netscape HTML)
bm import netscape ~/Downloads/bookmarks.html

# add a bookmark
bm add https://example.com -n "Example" -t ref,demo -d "Short note"

# list newest bookmarks (ID, path, title, URL)
bm list

# search across title/url/tags/body
bm search kernel

# search within a specific path
bm search kernel --path dev/linux

# list directory prefixes
bm dirs

# open the first result
ID=$(bm search kernel --jsonl | head -1 | jq -r '.id')
bm open "$ID"

# export for browsers (Netscape HTML)
bm export netscape > bookmarks.html

Concepts

Store layout

Default store directory is ~/.bookmarks.d (override via $BOOKMARKS_DIR). Each bookmark is a single .bm file; directories serve as namespaces.

~/.bookmarks.d/
  dev/python/fastapi-3a1b2c4.bm
  news-ycombinator-com-1234567.bm
  README.txt

File names are human readable and end with a short hash of the URL to avoid collisions.

Bookmark file format

Each .bm file contains front matter and an optional body:

---
url: https://example.com/blog/post
title: Great post
tags: [read, blog, "needs,comma"]
created: 2025-09-16T08:42:00-07:00
modified: 2025-09-17T09:10:00-07:00
---
Longer notes, checklists, code blocks…
  • tags is a list and supports quoting for commas/spaces
  • Any extra keys are preserved on round‑trip

IDs

Each bookmark has a stable ID derived from its URL (BLAKE2b short hash). The ID does not change if you rename/move the file. You can use either the ID or a path‑like slug with commands.


CLI usage

Run bm --help or bm <command> --help for command details.

init

Create a store; optional --git initializes a Git repo.

bm init --git

add

Add a bookmark. --edit opens your $EDITOR with a pre‑filled template.

bm add <url> [-n TITLE] [-t tag1,tag2] [-d NOTES] [-p dir1/dir2] [--id SLUG] [--edit] [-f]

Prints the stable ID on success.

list

List bookmarks (newest first).

bm list [--host HOST] [--since ISO|YYYY-MM-DD] [-t TAG] [--path PREFIX] [--json|--jsonl]

search

Full‑text search across title, url, tags, and body.

bm search <query> [--path PREFIX] [--json|--jsonl]

show and open

Display metadata/notes or open the URL in your default browser:

bm show <ID|path>
bm open <ID|path>

edit, rm, mv

bm edit <ID|path>   # bumps modified timestamp
bm rm <ID|path>
bm mv <SRC> <DST> [-f]

tags and tag add|rm

List discovered tags (from folder segments and header tags), or mutate tags without opening an editor.

bm tags
bm tag add <ID|path> tag1 tag2
bm tag rm  <ID|path> tag1

dirs

List all known directory prefixes in the bookmark store.

bm dirs [--json]

dedupe

Merge duplicate bookmarks that resolve to the same normalized URL. The command unions tags (including folder segments), keeps the most informative entry, and appends any extra notes with provenance markers.

bm dedupe [--dry-run] [--json]
  • --dry-run prints the planned merges without modifying files
  • --json emits a machine-readable summary of the merge actions

export and import

Netscape HTML (for browsers) and JSON exports; Netscape import with folder hierarchies preserved.

bm export netscape [--host HOST] [--since ISO|YYYY-MM-DD] > bookmarks.html
bm export json > dump.json
bm import netscape bookmarks.html [-f]

sync

If the store is a Git repo, stage/commit and (if upstream exists) push.

bm sync

Filtering & output formats

  • --host matches the URL host (case‑insensitive, ignores leading www.)
  • --path filters by path prefix (e.g., --path dev/python shows only entries under that directory tree)
  • --since accepts YYYY-MM-DD or full ISO timestamps; comparisons are proper datetimes
  • --json emits a single JSON array; --jsonl outputs one JSON object per line (NDJSON)

Common JSON schema fields: id, path, title, url, tags, created, modified.


Integration recipes

fzf launcher

bm list --jsonl | fzf --with-nth=2.. | awk '{print $1}' | xargs -r bm open

Open the latest saved from a host

bm list --host example.com --jsonl | head -1 | jq -r '.id' | xargs -r bm open

Rofi launcher

#!/bin/sh
choice="$(
    bm list --jsonl |
    jq -r '.id + "\t" + .title + " — " + .url' |
    rofi -dmenu -i -p "bm"
)"
if [ -n "$choice" ]; then
    bm open "$(printf "%s" "$choice" | cut -f1)"
fi

Save as bm-rofi.sh, make it executable (chmod +x bm-rofi.sh), and bind it to a hotkey. The tab delimiter keeps IDs intact even when titles contain spaces; rofi shows the full title and URL while bm open receives only the bookmark ID.

dmenu launcher

#!/bin/sh
choice="$(
    bm list --jsonl |
    jq -r '.id + "\t" + .title + " — " + .url' |
    dmenu -l 15 -i -p "bm"
)"
if [ -n "$choice" ]; then
    bm open "$(printf "%s" "$choice" | cut -f1)"
fi

You can adjust -l 15 to change the number of visible rows. Because the script uses tabs between the ID and the description, cut -f1 reliably extracts the ID even when titles or URLs contain spaces.

Bulk tag HN links

bm list --host news.ycombinator.com --jsonl | jq -r '.id' | xargs -n1 bm tag add hn

List bookmarks in a specific category

bm list --path dev/python
bm search "framework" --path dev

Explore directory structure

bm dirs
bm dirs --json | jq

Export → browser import

bm export netscape > ~/Desktop/bookmarks.html
# Import that file in your browser’s bookmarks manager

Sync with Syncthing

For cross-device synchronization without Git, use Syncthing to sync your bookmark store:

  1. Install Syncthing on all devices.
  2. Add your bookmark store directory (~/.bookmarks.d or $BOOKMARKS_DIR) as a synced folder in Syncthing.
  3. Configure devices to share the folder bidirectionally.
  4. Syncthing will keep your bookmarks in sync across devices automatically.

Auto-export for browser import

To automatically export bookmarks to Netscape HTML for browser import:

#!/bin/bash
# auto_export.sh
bm export netscape > ~/bookmarks_auto.html
echo "Bookmarks exported to ~/bookmarks_auto.html. Import this file in your browser."

Run this script periodically or on demand to generate an up-to-date bookmark file for browser import.


Configuration

  • Store directory: set BOOKMARKS_DIR or pass --store to any command
  • Editor: VISUAL or EDITOR (supports commands like code --wait)

Windows notes:

  • Paths avoid reserved names and use atomic replaces; long paths depend on OS settings

Security & robustness

  • Atomic writes: all modifications write to a temp file then os.replace it
  • Path safety: .. and absolute paths are rejected; files cannot escape the store
  • No network by default: bm never fetches content (future hooks can)
  • Git: pushes only if an upstream is configured

Migration notes

From older versions where IDs depended on path: IDs are now URL‑only (stable across renames). Old files remain valid; tags as comma strings are still read and normalized to lists on save.


Development

# lint (optional) — stdlib only, so just run the script
python3 -m compileall src

# run tests (if added)
pytest -q

Roadmap / ideas

  • bm reindex + optional on‑disk index for very large stores
  • Markdown/CSV exports
  • Simple HTTP UI (bm serve) and browser extension hooks
  • Optional encryption (GPG or git‑crypt) for private notes

License

MIT. Do what you want; a credit is appreciated.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bkmrk-0.2.1.tar.gz (33.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bkmrk-0.2.1-py3-none-any.whl (22.4 kB view details)

Uploaded Python 3

File details

Details for the file bkmrk-0.2.1.tar.gz.

File metadata

  • Download URL: bkmrk-0.2.1.tar.gz
  • Upload date:
  • Size: 33.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for bkmrk-0.2.1.tar.gz
Algorithm Hash digest
SHA256 7868c080cd069a84fedf6fc0638a276c391178552f7956e29bd7c6693ceb59d7
MD5 7bac3283d1743327f7e3dad88d03f851
BLAKE2b-256 a856545abbb04d273353f40a6090a64091acbcb42404a412751431ec3c2f3e13

See more details on using hashes here.

Provenance

The following attestation bundles were made for bkmrk-0.2.1.tar.gz:

Publisher: publish.yml on jtabke/bkmrk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file bkmrk-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: bkmrk-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 22.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for bkmrk-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e9bf3b615a9bf49760e2537ad082f11d11ab700d206241a58f37f58eefcc82c0
MD5 337b39c972c8939cb1c0961bc5de9624
BLAKE2b-256 e402276c980e8fdfe988ef96afefc851ef69f79a404fcf89604d4869fb4efbed

See more details on using hashes here.

Provenance

The following attestation bundles were made for bkmrk-0.2.1-py3-none-any.whl:

Publisher: publish.yml on jtabke/bkmrk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page