Skip to main content

Projects

·1860 words·9 mins·
Table of Contents

Most of what I have built belongs to somebody else. These are the ones that do not — small tools written to scratch my own itch, kept public in case the itch was not only mine.

They are all works in progress, and none of them are asking to be depended on. Have a look around anyway; issues and pull requests are welcome.

Repositories
#

org-roam-to-obsidian

Points at an org-roam vault and hands back notes Obsidian can open. ID links become wikilinks, org properties become YAML front matter, source blocks become fenced code, and the org-mode scaffolding is dropped on the way through. Standard library only, so there is nothing to install before you run it.
python3 convert.py --help
usage: convert.py [-h] [-i INPUT] [-o OUTPUT] [-p PROPERTIES [PROPERTIES ...]]
                  [--no-created] [--title-heading] [--keep-dead-links]
                  [--publish-when PROPERTY=VALUE] [--publish-key KEY]
                  [--created-key KEY [KEY ...]] [--modified-key KEY [KEY ...]]
                  [--no-modified] [--created-property CREATED_PROPERTY]

Convert org-roam files to Obsidian markdown format

options:
  -h, --help            show this help message and exit
  -i, --input INPUT     Input directory containing org-roam files (default:
                        ~/Documents/slip-box)
  -o, --output OUTPUT   Output directory for markdown files (default:
                        ./output)
  -p, --properties PROPERTIES [PROPERTIES ...]
                        Org-mode properties to extract (e.g., filetags
                        roam_refs)
  --no-created          Disable adding created timestamp from filename
  --title-heading       Repeat the title as an H1 at the top of the body. Off
                        by default, because Obsidian and Quartz already show
                        it
  --keep-dead-links     Keep wikilinks whose target does not exist, instead of
                        unwrapping them
  --publish-when PROPERTY=VALUE
                        Mark a note for publishing when it carries this org
                        property value, for example category=public
  --publish-key KEY     Frontmatter key for the publish flag (default:
                        publish)
  --created-key KEY [KEY ...]
                        One or more frontmatter keys for the created timestamp
                        (default: created). Quartz also reads created_at and
                        date
  --modified-key KEY [KEY ...]
                        One or more frontmatter keys for the modified
                        timestamp (default: modified). Quartz also reads
                        lastmod, updated and last-modified
  --no-modified         Disable adding the modified timestamp from the org
                        file's mtime
  --created-property CREATED_PROPERTY
                        Use a specific org-mode property for created timestamp
                        instead of filename

PythonGPL-3.0

gwttr

A small command-line weather client for wttr.in. The client is a proof of concept and stays that way on purpose — the point was somewhere to hang a real Go toolchain, so it comes with tests, coverage reporting and a published module.
./gwttr
Weather report: honolulu

      \   /     Clear
       .-.      26 °C
    ― (   ) ―    23 km/h
       `-’      10 km
      /   \     0.0 mm

GoGPL-3.0

Washy Washy

washy-washy-cli

Turns a CSV of laundry piles into two PDFs — one tall page for your phone at the machine, one A4 sheet to pin beside it. Every card is drawn for your appliances: you describe the washing machine and iron once in JSON, and each pile then shows the programme dial with the pointer where you turn it, the temperature and spin, which buttons to press, and the iron’s thermostat ring. Fascia labels are never translated — if the dial says Fijn/Zijde, so does the card.
Two washy-washy instruction cards side by side, each with a drawn programme dial, temperature and spin chips, and iron guidance
bun run generate --help
Usage: bun run generate [csv] [--out <dir>] [--machine <file>]

  csv               instruction CSV to read (default: data/washing-instructions.csv,
                    falling back to the committed .dist)
  --out <dir>       where the six PDFs go (default: out)
  --machine <file>  the appliances to draw (default: data/machine.json,
                    falling back to the committed .dist)

TypeScriptGPL-3.0

washy-washy-web

Live demo
The washy-washy laundry chart as a web page instead of a PDF — filter it by pile or by what’s due for a wash, download it, or share the filtered view as a link. Add it to your phone’s home screen so it opens by the machine with no address bar. Upload your own chart and machine file, or edit every field — piles, programmes, temperatures — right in the browser, all kept on your device rather than a server.
The washy-washy web app showing the bundled example laundry chart, with a filter panel for cutting it down by pile or by wash, and buttons to download the sheet as a PDF or share the current view The washing loads editor: upload or download a config, a summary of the current machine, and editable cards for every pile in the chart The machine editor: name, capacity, dial programmes in order and temperatures, all editable fields for describing your own washer and iron

AstroGPL-3.0

washy-washy-core

Chart parsing, machine validation and mixing rules for washy-washy, published on npm as @washy-washy/core — checks a CSV or JSON laundry chart against your washing machine and iron, and works out which piles can share a drum. One entry point for Node or Bun with CSV parsing, and a browser build that reads and writes charts as JSON instead.

TypeScriptGPL-3.0

form-handler

Turns a contact form submission into an email, which is the whole job. It exists because a static site cannot open an SMTP connection and the mail for these domains is self-hosted, so routing the form through a third party was the alternative. One instance serves any number of forms across any number of domains, each with its own allowed origins, so one site cannot post to another’s form.
form-handler --help
Accepts contact form submissions over HTTP and emails them on.

Configured from the environment, or from a forms file when FORMS_FILE
is set. See the README for the variables it reads.

Usage:
  form-handler [flags]

Flags:
      --healthcheck   probe the local /healthz and exit
  -h, --help          help for form-handler
  -v, --version       version for form-handler

GoGPL-3.0

backup-git-repos

Mirrors every repository on a self-hosted GitLab or Forgejo instance, or a GitHub.com account, to a local, restorable copy — a bare clone of every branch and tag, kept under the forge’s own namespace structure. It tells archived repositories from active ones, refreshes existing mirrors incrementally instead of re-cloning, and can write either set out as a .tar.gz alongside the mirror.
backup-git-repos --help
Back up git repositories from GitLab, Forgejo, and GitHub

Usage:
  backup-git-repos [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  config      Manage the backup-git-repos config file
  help        Help about any command
  list        Print what would be backed up, clone nothing
  run         Mirror repositories from the configured forges
  version     Print the version

Flags:
  -c, --config string   config file (default: $XDG_CONFIG_HOME/backup-git-repos/config.yaml)
  -h, --help            help for backup-git-repos

Use "backup-git-repos [command] --help" for more information about a command.

GoGPL-3.0

linkwarden-obsidian-sync

Keeps a directory of Obsidian notes in sync with your saved Linkwarden links: one note per link, added, updated or removed to match Linkwarden’s current state exactly. No LLM in the loop — everything is a deterministic API call and a file write, meant to run unattended on a schedule such as a systemd --user timer. It doesn’t touch git; reviewing and committing whatever changed is your own workflow from there.
linkwarden-obsidian-sync
2026/08/30 linkwarden-obsidian-sync 1.3.0
2026/08/30 synced 2 link(s): 1 added, 1 updated, 1 removed

GoGPL-3.0

Hush-Hush

Hush-Hush

A lightweight, standalone secrets object store: injecting a value seals it with public-key encryption (age) to whichever recipient(s) the writer chooses, and the server only ever stores and serves that ciphertext — it never touches a private key or a plaintext value. Ships container-only; every fetch is audit logged.

GoGPL-3.0

hush-hush-cli

The client for Hush-Hush: inject a secret, fetch and decrypt one, rotate a value, delete an object. age keeps the private keys and the plaintext local — this CLI only ever handles already-sealed ciphertext outside a single inject/get call.
hush-hush-cli --help
Client for the hush-hush secrets object store

Usage:
  hush-hush-cli [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  delete      Permanently remove an object
  get         Fetch and decrypt one value to stdout
  help        Help about any command
  init        Write a starter config file
  inject      Seal a value (read from stdin) and create a new object
  update      Seal a new value (read from stdin) and replace an object's stored value

Flags:
      --caller string          self-presented identity recorded in the audit log
  -h, --help                   help for hush-hush-cli
      --server string          hush-hush server URL (default "http://localhost:8080")
      --token string           write-path bearer token
      --token-command string   command whose trimmed stdout is the write-path bearer token (wins over --token if both are set)
  -v, --version                version for hush-hush-cli
  -y, --yes                    write a starter config with no prompt, if none exists

Use "hush-hush-cli [command] --help" for more information about a command.

GoGPL-3.0

hush-hush-go

The official Go SDK for Hush-Hush, generated from its OpenAPI spec and kept in sync with it automatically. go get github.com/alrayyes/hush-hush-go — Go modules resolve straight from source control, so there’s no separate package registry to link.

GoMIT

movie-planner

Logs the movies you’ve watched — title, date, start/end time, where you watched it — and syncs each viewing to a Baikal (CalDAV) calendar. Replaces a hand-maintained log with a guided prompt, enriches entries with IMDb/Rotten Tomatoes/Metacritic ratings via OMDb and a manually entered Letterboxd link, and catches accidental duplicate entries with fuzzy title matching.
movie-planner --help
 Usage: movie-planner [OPTIONS] COMMAND [ARGS]...                           
                                                                            
 movie-planner: log watched movies and sync them to a calendar.             
                                                                            
╭─ Options ────────────────────────────────────────────────────────────────╮
│ --config                      Path to config.toml. Defaults to the │
│                                     XDG config location.                 │
│                                     [env var: MOVIE_PLANNER_CONFIG]      │
│ --caldav-url                   Override caldav.url from the config  │
│                                     file.                                │
│                                     [env var: MOVIE_PLANNER_CALDAV_URL]  │
│ --caldav-username              Override caldav.username from the    │
│                                     config file.                         │
│                                     [env var:                            │
│                                     MOVIE_PLANNER_CALDAV_USERNAME]       │
│ --omdb-api-key                 Override omdb.api_key from the       │
│                                     config file.                         │
│                                     [env var:                            │
│                                     MOVIE_PLANNER_OMDB_API_KEY]          │
│ --db-path                     Override storage.db_path from the    │
│                                     config file.                         │
│                                     [env var: MOVIE_PLANNER_DB_PATH]     │
│ --install-completion                Install completion for the current   │
│                                     shell.                               │
│                                     [env var:                            │
│                                     MOVIE_PLANNER_INSTALL_COMPLETION]    │
│ --show-completion                   Show completion for the current      │
│                                     shell, to copy it or customize the   │
│                                     installation.                        │
│                                     [env var:                            │
│                                     MOVIE_PLANNER_SHOW_COMPLETION]       │
│ --help                              Show this message and exit.          │
╰──────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────╮
│ init       Write a starter config.toml, ready to edit.                   │
│ log        Interactively log a watched movie.                            │
│ list       List logged entries.                                          │
│ update     Update an existing logged entry.                              │
│ delete     Delete a logged entry.                                        │
│ import     Bulk import viewing entries from a CSV or JSON file.          │
│ locations  Manage the medium and venue lists.                            │
│ sync       Manage calendar sync.                                         │
╰──────────────────────────────────────────────────────────────────────────╯

PythonGPL-3.0