commit b31ff96374603a56ca943dbd9aec8d980b92f0bb
parent 901745c27ec568193ab0ba1fe8af9165c0850bbd
Author: mtmn <miro@haravara.org>
Date: Fri, 8 May 2026 11:45:33 +0200
feat: add readme, move shirts, deprecate mold
Diffstat:
27 files changed, 343 insertions(+), 511 deletions(-)
diff --git a/MODULE.bazel b/MODULE.bazel
@@ -15,7 +15,6 @@ rust.toolchain(
edition = "2024",
extra_rustc_flags = [
"-Ctarget-cpu=native",
- "-Clink-arg=-fuse-ld=mold",
],
extra_target_triples = [],
versions = ["nightly/2026-04-25"],
diff --git a/README.md b/README.md
@@ -1,41 +1,45 @@
# tools
-### bootstrap
-```bash
-sudo pacman -Sy bazelisk mold
-bazel fetch //...
-```
+Various tools I have been using throughout the years.
-### query
-```bash
-bazel query "kind('rust_', //...) except attr('name', '.*-(debug|profiling)$', //...)"
-bazel query "kind('zig_', //...)"
-bazel query "kind('py_', //...)"
-bazel query "kind('cc_', //...)"
-bazel query "kind('sh_', //...)"
-bazel query "kind('rb_', //...)"
-```
+| tool | description |
+|---|---|
+| [aaltomuoto](aaltomuoto/) | frequency-colored audio waveform generator |
+| [backup](backup/) | backup orchestrator — git, plakar, S3 |
+| [bandeno](bandeno/) | Bandcamp JSON Feed proxy |
+| [diffamer](diffamer/) | line-file syncer with rsync and coloured diff |
+| [diggah](diggah/) | find files modified within a time range, locally or over SSH |
+| [hue](hue/) | Ruby library and CLI for Philips Hue |
+| [lastfm_rs](lastfm_rs/) | Last.fm scrobble exporter to CSV |
+| [lazymaster](lazymaster/) | 2-pass audio loudness normalization via ffmpeg |
+| [mpd](mpd/) | MPD utility tools |
+| [nts](nts/) | NTS show tracklist fetcher |
+| [pmn](pmn/) | password selection utility for pass (Wayland, X11, macOS) |
+| [pracomer](pracomer/) | Pomodoro timer |
+| [shuffle](shuffle/) | random album picker for MPD |
+| [speediness](speediness/) | network speed test |
+| [tempfox](tempfox/) | Firefox user.js preference overrides |
+| [wlr-river-title](wlr-river-title/) | River/Wayland focused view title printer |
+
+## Building
-### build
-```bash
-bazel build //magnolia
+Install [Bazelisk](https://github.com/bazelbuild/bazelisk), then fetch dependencies:
+
+```sh
+bazelisk fetch //...
```
-### deploy
-> `bazel-bin` -> ``$HOME/.local/share/tools``
-```bash
-sudo install -d -m 700 -o $USER $HOME/.local/share/tools
-bazel run //magnolia:deploy
+Build all tools:
+
+```sh
+bazelisk build //...
```
-### misc
-```bash
-# updates root `Cargo.lock` (rust tools are nested in a workspace)
-cargo generate-lockfile
-# builds everything (including `debug` and `profiling` binaries)
-bazel build //...
-# updates python packages (`requirements.in` -> `requirements.out`)
-bazel run //:requirements.update
-# runs a `justfile` that contains all of the necessary commands
-just
+## Deploying
+
+Installs built binaries to `$HOME/.local/share/tools`:
+
+```sh
+sudo install -d -m 700 -o $USER $HOME/.local/share/tools
+bazelisk run //magnolia:deploy
```
diff --git a/backup/README.md b/backup/README.md
@@ -0,0 +1,35 @@
+# backup
+
+A backup orchestrator that commits and pushes git repos, snapshots directories with [plakar](https://github.com/PlakarKorp/plakar), and syncs files to S3 via `aws s3 sync`.
+
+## Building
+
+```sh
+cabal build
+```
+
+## Usage
+
+```sh
+backup <type> [--push]
+```
+
+### Types
+
+| Type | Description |
+|------|-------------|
+| `push` | Commit and push all git repos |
+| `git` | Commit all git repos (push with `--push`) |
+| `annex` | Sync all git-annex repos |
+| `local` | Snapshot `$HOME` to the local plakar store |
+| `plakar` | Snapshot all configured directories to the remote plakar store |
+| `mobius` | Sync all configured directories to S3 |
+| `ls` | List all configured backup targets |
+| `<name>` | Run backup for a specific named target |
+
+### Options
+
+| Flag | Description |
+|------|-------------|
+| `--push` | Push after committing (for git targets) |
+| `-h`, `--help` | Show help |
diff --git a/bazel/rust.bzl b/bazel/rust.bzl
@@ -19,7 +19,6 @@ def rust_app(
common_flags = [
"-Ctarget-cpu=native",
- "-Clink-arg=-fuse-ld=mold",
] + rustc_flags
rust_binary(
@@ -95,7 +94,6 @@ def rust_lib(
common_flags = [
"-Ctarget-cpu=native",
- "-Clink-arg=-fuse-ld=mold",
] + rustc_flags
rust_library(
diff --git a/diffamer/README.md b/diffamer/README.md
@@ -0,0 +1,38 @@
+# diffamer
+
+Syncs line-oriented text files between a local directory and a remote host over rsync. By default shows a coloured diff; pass `--sync` to apply changes.
+
+Merge strategy: union of local and remote lines, deduplicated and sorted with natural ordering.
+
+## Building
+
+```sh
+cargo build --release
+```
+
+## Usage
+
+```sh
+diffamer --host <host> --local <dir> (--remote <dir> | --same-as-local) [--sync] [--reverse]
+```
+
+### Options
+
+| Flag | Description |
+|------|-------------|
+| `--host` | SSH host alias to sync with |
+| `--local` | Local directory containing the text files |
+| `--remote` | Remote path to sync from/to |
+| `--same-as-local` | Use the same path on the remote as `--local` |
+| `--sync` | Apply changes (default: dry-run diff only) |
+| `--reverse` | Merge local → remote instead of remote → local |
+
+### Example
+
+```sh
+# Preview changes from remote
+diffamer --host myserver --local ~/lists --same-as-local
+
+# Apply changes
+diffamer --host myserver --local ~/lists --same-as-local --sync
+```
diff --git a/lastfm_rs/README.md b/lastfm_rs/README.md
@@ -0,0 +1,32 @@
+# lastfm_rs
+
+Exports Last.fm scrobbles to a CSV file. Output format is compatible with [Maloja](https://github.com/krateng/maloja).
+
+## Building
+
+```sh
+cargo build --release
+```
+
+## Usage
+
+```sh
+lastfm_rs --username <user> --api-key <key> [options]
+```
+
+### Options
+
+| Flag | Default | Description |
+|------|---------|-------------|
+| `-u`, `--username` | — | Last.fm username |
+| `-a`, `--api-key` | — | Last.fm API key |
+| `-o`, `--output` | `scrobbles.csv` | Output CSV file path |
+| `--from` | — | Start time (Unix timestamp) |
+| `--to` | — | End time (Unix timestamp) |
+| `-l`, `--limit` | — | Max pages to fetch (200 tracks/page) |
+
+### Example
+
+```sh
+lastfm_rs --username miro --api-key abc123 --output scrobbles.csv
+```
diff --git a/mpd/README.md b/mpd/README.md
@@ -0,0 +1,54 @@
+# mpd
+
+A collection of C utilities for controlling [MPD](https://www.musicpd.org/) via libmpdclient.
+
+## Tools
+
+### mpd_add_to_queue
+
+Adds tracks to the MPD play queue. Reads a list of paths from a file or stdin.
+
+```sh
+mpd_add_to_queue [file]
+```
+
+### mpd_add_to_playlist
+
+Adds tracks to a named MPD playlist. Reads a list of paths from a file or stdin.
+
+```sh
+mpd_add_to_playlist [file]
+```
+
+### mpd_edit_queue
+
+Opens the current MPD queue in `$EDITOR` as a list of file paths. Saves and replaces the queue with the edited result, preserving the currently playing track's position where possible.
+
+```sh
+mpd_edit_queue
+```
+
+### mpd_update_library
+
+Triggers an MPD library update and waits for it to complete, retrying on connection errors with exponential backoff.
+
+```sh
+mpd_update_library
+```
+
+### mpd_update_queue
+
+Replaces the current MPD queue with a new list of tracks read from a file or stdin.
+
+```sh
+mpd_update_queue [file]
+```
+
+## Configuration
+
+All tools read connection settings from environment variables.
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `MPD_HOST` | `localhost` | MPD server hostname |
+| `MPD_PORT` | `6600` | MPD server port |
diff --git a/nts/README.md b/nts/README.md
@@ -0,0 +1,24 @@
+# nts
+
+Fetches the tracklist for an [NTS Radio](https://www.nts.live/) episode and saves it as a text file.
+
+## Usage
+
+```sh
+nts <episode-url>
+```
+
+The tracklist is written to `<episode-name>.txt` in the current directory and also printed to stdout.
+
+### Example
+
+```sh
+nts https://www.nts.live/shows/some-show/episodes/some-episode-01-01-2025
+# writes some-episode-01-01-2025.txt
+```
+
+## Options
+
+| Flag | Description |
+|------|-------------|
+| `--debug` | Print debug output to stderr |
diff --git a/pracomer/README.md b/pracomer/README.md
@@ -0,0 +1,33 @@
+# pracomer
+
+A Pomodoro timer for the terminal. Alternates between work and break intervals, printing the current status and sending desktop notifications at each transition.
+
+## Building
+
+```sh
+zig build
+```
+
+## Usage
+
+```sh
+pracomer [-t <minutes>] [-b <minutes>] [-s] [--file <path>]
+```
+
+### Options
+
+| Flag | Default | Description |
+|------|---------|-------------|
+| `-t <minutes>` | `26` | Work interval length in minutes |
+| `-b <minutes>` | `6` | Break interval length in minutes |
+| `-s` | — | Disable desktop notifications |
+| `--file <path>` | — | Write current status to a file (useful for status bars) |
+| `-h` | — | Show help |
+
+### Controls
+
+| Key | Action |
+|-----|--------|
+| `p` | Pause / resume |
+| `r` | Reset current interval |
+| `q` | Quit |
diff --git a/shirts/.gitignore b/shirts/.gitignore
@@ -1,4 +0,0 @@
-/.expert/
-/_build/
-/deps/
-khal_notifier
diff --git a/shirts/BUILD b/shirts/BUILD
@@ -1,13 +0,0 @@
-load("//bazel:local-deploy.bzl", "local_deploy")
-
-# you have to `mix escript.build` first and then copy the binary to /opt/tools
-filegroup(
- name = "binary",
- srcs = ["shirt_linkener"],
- visibility = ["//visibility:public"],
-)
-
-local_deploy(
- name = "deploy",
- srcs = [":binary"],
-)
diff --git a/shirts/Containerfile b/shirts/Containerfile
@@ -1,20 +0,0 @@
-FROM docker.io/elixir:1.19-slim
-
-ENV MIX_ENV=prod
-ENV DEBIAN_FRONTEND=noninteractive
-
-RUN apt-get update && apt-get install -y --no-install-recommends \
- ca-certificates \
- && rm -rf /var/lib/apt/lists/
-
-WORKDIR /app
-COPY mix.exs mix.lock ./
-
-RUN mix local.hex --force && mix local.rebar --force && MIX_ENV=prod mix deps.get --only prod
-
-COPY config ./config
-COPY lib ./lib
-
-RUN MIX_ENV=prod mix compile
-
-CMD ["mix", "run", "--no-halt"]
diff --git a/shirts/Makefile b/shirts/Makefile
@@ -1,32 +0,0 @@
-.PHONY: all build format lint check clean run
-
-AUTH_TOKEN := demo
-BASE_URL := http://localhost
-PORT := 4000
-DB_PATH := shortener.db
-
-export AUTH_TOKEN
-export BASE_URL
-export PORT
-export DB_PATH
-
-all: build
-
-build:
- mix deps.get
- mix compile
-
-run:
- mix run --no-halt
-
-format:
- mix format
-
-lint:
- mix credo --strict
-
-check:
- mix dialyzer
-
-clean:
- mix clean
diff --git a/shirts/README.md b/shirts/README.md
@@ -1,46 +0,0 @@
-# shirts
-
-A URL shortener. Links are stored in a SQLite database and served under a configurable base URL.
-
-## Building
-
-```sh
-mix deps.get
-mix release
-```
-
-## Usage
-
-### Create a short link
-
-```sh
-curl -X POST http://localhost:8721/curtail \
- -H "Authorization: Bearer <token>" \
- -H "Content-Type: application/json" \
- -d '{"url": "https://example.com/very/long/path"}'
-```
-
-Optionally provide a custom code:
-
-```sh
-curl -X POST http://localhost:8721/curtail \
- -H "Authorization: Bearer <token>" \
- -H "Content-Type: application/json" \
- -d '{"url": "https://example.com/very/long/path", "code": "my-code"}'
-```
-
-### Follow a short link
-
-```
-GET /s/<code>
-```
-
-## Configuration
-
-| Variable | Description |
-|------------|--------------------------------------|
-| `PORT` | Port to listen on (default: `8721`) |
-| `HOST` | Host to bind to (default: `127.0.0.1`) |
-| `BASE_URL` | Base URL used when generating links |
-| `AUTH_TOKEN` | Bearer token for the create endpoint |
-| `DB_PATH` | Path to the SQLite database file |
diff --git a/shirts/config/config.exs b/shirts/config/config.exs
@@ -1,7 +0,0 @@
-import Config
-
-config :shortener,
- port: System.get_env("PORT", "4000") |> String.to_integer(),
- auth_token: System.get_env("AUTH_TOKEN", "changeme"),
- db_path: System.get_env("DB_PATH", "data.db"),
- base_url: System.get_env("BASE_URL", "http://localhost")
diff --git a/shirts/config/runtime.exs b/shirts/config/runtime.exs
@@ -1,10 +0,0 @@
-import Config
-
-if config_env() == :prod do
- config :shortener,
- port: System.get_env("PORT") |> then(&(if &1, do: String.to_integer(&1), else: 4000)),
- host: System.get_env("HOST") || "127.0.0.1",
- auth_token: System.fetch_env!("AUTH_TOKEN"),
- db_path: System.get_env("DB_PATH") || "data.db",
- base_url: System.get_env("BASE_URL") || "http://localhost"
-end
diff --git a/shirts/flake.lock b/shirts/flake.lock
@@ -1,27 +0,0 @@
-{
- "nodes": {
- "nixpkgs": {
- "locked": {
- "lastModified": 1778124196,
- "narHash": "sha256-pYEytCNic/czazbV9r3tbQ6BZzqRBg/41x2dIC5ymOo=",
- "owner": "NixOS",
- "repo": "nixpkgs",
- "rev": "68a8af93ff4297686cb68880845e61e5e2e41d92",
- "type": "github"
- },
- "original": {
- "owner": "NixOS",
- "ref": "nixpkgs-unstable",
- "repo": "nixpkgs",
- "type": "github"
- }
- },
- "root": {
- "inputs": {
- "nixpkgs": "nixpkgs"
- }
- }
- },
- "root": "root",
- "version": 7
-}
diff --git a/shirts/flake.nix b/shirts/flake.nix
@@ -1,28 +0,0 @@
-{
- inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
-
- outputs = {nixpkgs, ...}: {
- packages = nixpkgs.lib.genAttrs ["x86_64-linux" "aarch64-linux"] (system: let
- pkgs = nixpkgs.legacyPackages.${system};
- in {
- default = pkgs.beamPackages.mixRelease {
- pname = "shortener";
- version = "0.0.1";
- src = ./.;
-
- nativeBuildInputs = [pkgs.beamPackages.elixir_1_18];
-
- MIX_HOME = "nix-mix";
- HEX_HOME = "nix-hex";
- ELIXIR_MAKE_CACHE_DIR = "nix-elixir-make";
-
- mixFodDeps = pkgs.beamPackages.fetchMixDeps {
- pname = "shortener-deps";
- version = "0.0.1";
- src = ./.;
- sha256 = "sha256-U/tNP3s7b4bBUrQx5l4VtDHpV9DsWj6zBYj8jsM4Acw=";
- };
- };
- });
- };
-}
diff --git a/shirts/lib/shortener/application.ex b/shirts/lib/shortener/application.ex
@@ -1,37 +0,0 @@
-defmodule Shortener.Application do
- @moduledoc """
- Shortener application entry point.
- Starts the database and the HTTP server (Bandit).
- """
- use Application
- require Logger
-
- @impl true
- def start(_type, _args) do
- port = Application.fetch_env!(:shortener, :port)
- host = Application.fetch_env!(:shortener, :host)
- db_path = Application.fetch_env!(:shortener, :db_path)
-
- children = [
- {Shortener.DB, db_path},
- {Bandit, plug: Shortener.Router, port: port, ip: parse_ip(host)}
- ]
-
- Logger.info("Starting shortener on #{host}:#{port}")
-
- Supervisor.start_link(children, strategy: :one_for_one, name: Shortener.Supervisor)
- end
-
- @impl true
- def stop(_state) do
- Logger.info("Shortener application stopped")
- :ok
- end
-
- defp parse_ip(ip_str) do
- case :inet.parse_address(to_charlist(ip_str)) do
- {:ok, ip} -> ip
- _ -> {127, 0, 0, 1}
- end
- end
-end
diff --git a/shirts/lib/shortener/db.ex b/shirts/lib/shortener/db.ex
@@ -1,83 +0,0 @@
-defmodule Shortener.DB do
- @moduledoc """
- SQLite database interface using Exqlite.
- Stores and retrieves short links.
- """
-
- use GenServer
- require Logger
-
- def start_link(db_path) do
- GenServer.start_link(__MODULE__, db_path, name: __MODULE__)
- end
-
- @impl true
- def init(db_path) do
- Process.flag(:trap_exit, true)
- {:ok, conn} = Exqlite.Sqlite3.open(db_path)
-
- :ok =
- Exqlite.Sqlite3.execute(conn, """
- CREATE TABLE IF NOT EXISTS shirts (
- code TEXT PRIMARY KEY,
- url TEXT NOT NULL,
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
- )
- """)
-
- Logger.info("Database ready at #{db_path}")
- {:ok, conn}
- end
-
- @doc "Insert a new short code → url mapping. Returns :ok or {:error, reason}."
- def insert(code, url) do
- GenServer.call(__MODULE__, {:insert, code, url})
- end
-
- @doc "Look up a URL by short code. Returns {:ok, url} or :not_found."
- def lookup(code) do
- GenServer.call(__MODULE__, {:lookup, code})
- end
-
- # --- server callbacks ---
-
- @impl true
- def handle_call({:insert, code, url}, _from, conn) do
- result =
- with {:ok, stmt} <-
- Exqlite.Sqlite3.prepare(conn, "INSERT INTO shirts (code, url) VALUES (?1, ?2)"),
- :ok <- Exqlite.Sqlite3.bind(stmt, [code, url]),
- :done <- Exqlite.Sqlite3.step(conn, stmt) do
- _ = Exqlite.Sqlite3.release(conn, stmt)
- :ok
- else
- {:error, reason} -> {:error, reason}
- end
-
- {:reply, result, conn}
- end
-
- def handle_call({:lookup, code}, _from, conn) do
- result =
- with {:ok, stmt} <-
- Exqlite.Sqlite3.prepare(conn, "SELECT url FROM shirts WHERE code = ?1"),
- :ok <- Exqlite.Sqlite3.bind(stmt, [code]),
- step_result <- Exqlite.Sqlite3.step(conn, stmt) do
- _ = Exqlite.Sqlite3.release(conn, stmt)
-
- case step_result do
- {:row, [url]} -> {:ok, url}
- :done -> :not_found
- {:error, reason} -> {:error, reason}
- end
- end
-
- {:reply, result, conn}
- end
-
- @impl true
- def terminate(_reason, conn) do
- Logger.info("Closing database connection")
- Exqlite.Sqlite3.close(conn)
- end
-end
diff --git a/shirts/lib/shortener/router.ex b/shirts/lib/shortener/router.ex
@@ -1,111 +0,0 @@
-defmodule Shortener.Router do
- @moduledoc """
- Plug router:
- POST /curtail — create a short link (requires Bearer token)
- GET /s/:code — redirect to the original URL
- """
-
- use Plug.Router
-
- plug Plug.Logger
- plug :match
- plug Plug.Parsers, parsers: [:json], json_decoder: Jason
- plug :dispatch
-
- # ---------------------------------------------------------------------------
- # POST /l/curtail
- # Body (JSON): {"url": "https://example.com/very/long/path"}
- # Optional: {"url": "...", "code": "my-custom-code"}
- # Header: Authorization: Bearer <token>
- # ---------------------------------------------------------------------------
- post "/curtail" do
- with :ok <- check_auth(conn),
- {:ok, url} <- fetch_url(conn),
- code <- Map.get(conn.body_params, "code") || random_code(),
- :ok <- Shortener.DB.insert(code, url) do
- base_url = Application.fetch_env!(:shortener, :base_url)
- short_url = "#{base_url}/s/#{code}"
-
- conn
- |> put_resp_content_type("application/json")
- |> send_resp(201, Jason.encode!(%{short_url: short_url, code: code, url: url}))
- else
- {:error, :unauthorized} ->
- json(conn, 401, %{error: "unauthorized"})
-
- {:error, :missing_url} ->
- json(conn, 422, %{error: "missing required field: url"})
-
- {:error, :invalid_url} ->
- json(conn, 422, %{error: "url must be a valid http or https URL"})
-
- {:error, _reason} ->
- json(conn, 409, %{error: "could not save link"})
- end
- end
-
- # ---------------------------------------------------------------------------
- # GET /s/:code — redirect
- # ---------------------------------------------------------------------------
- get "/s/:code" do
- case Shortener.DB.lookup(code) do
- {:ok, url} ->
- conn
- |> put_resp_header("location", url)
- |> send_resp(301, "")
-
- :not_found ->
- json(conn, 404, %{error: "not found"})
-
- {:error, _} ->
- json(conn, 500, %{error: "internal error"})
- end
- end
-
- # ---------------------------------------------------------------------------
- # Catch-all
- # ---------------------------------------------------------------------------
- match _ do
- json(conn, 404, %{error: "not found"})
- end
-
- # ---------------------------------------------------------------------------
- # Helpers
- # ---------------------------------------------------------------------------
-
- defp check_auth(conn) do
- expected = "Bearer #{Application.fetch_env!(:shortener, :auth_token)}"
-
- case get_req_header(conn, "authorization") do
- [^expected] -> :ok
- _ -> {:error, :unauthorized}
- end
- end
-
- defp fetch_url(conn) do
- case conn.body_params do
- %{"url" => url} when is_binary(url) and url != "" ->
- uri = URI.parse(url)
-
- if uri.scheme in ["http", "https"] and is_binary(uri.host) and uri.host != "" do
- {:ok, url}
- else
- {:error, :invalid_url}
- end
-
- _ ->
- {:error, :missing_url}
- end
- end
-
- defp random_code do
- :crypto.strong_rand_bytes(6)
- |> Base.url_encode64(padding: false)
- end
-
- defp json(conn, status, body) do
- conn
- |> put_resp_content_type("application/json")
- |> send_resp(status, Jason.encode!(body))
- end
-end
diff --git a/shirts/mix.exs b/shirts/mix.exs
@@ -1,31 +0,0 @@
-defmodule Shortener.MixProject do
- use Mix.Project
-
- def project do
- [
- app: :shortener,
- version: "0.0.1",
- elixir: "~> 1.18",
- start_permanent: Mix.env() == :prod,
- deps: deps()
- ]
- end
-
- def application do
- [
- extra_applications: [:logger],
- mod: {Shortener.Application, []}
- ]
- end
-
- defp deps do
- [
- {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
- {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
- {:bandit, "~> 1.10"},
- {:plug, "~> 1.19"},
- {:exqlite, "~> 0.35"},
- {:jason, "~> 1.4"}
- ]
- end
-end
diff --git a/shirts/mix.lock b/shirts/mix.lock
@@ -1,27 +0,0 @@
-%{
- "bandit": {:hex, :bandit, "1.10.3", "1e5d168fa79ec8de2860d1b4d878d97d4fbbe2fdbe7b0a7d9315a4359d1d4bb9", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "99a52d909c48db65ca598e1962797659e3c0f1d06e825a50c3d75b74a5e2db18"},
- "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
- "burrito": {:hex, :burrito, "1.5.0", "d68ec01df2871f1d5bc603b883a78546c75761ac73c1bec1b7ae2cc74790fcd1", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, ">= 0.5.0", [hex: :req, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.2.0 or ~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "3861abda7bffa733862b48da3e03df0b4cd41abf6fd24b91745f5c16d971e5fa"},
- "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
- "credo": {:hex, :credo, "1.7.16", "a9f1389d13d19c631cb123c77a813dbf16449a2aebf602f590defa08953309d4", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0562af33756b21f248f066a9119e3890722031b6d199f22e3cf95550e4f1579"},
- "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
- "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
- "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
- "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"},
- "exqlite": {:hex, :exqlite, "0.35.0", "90741471945db42b66cd8ca3149af317f00c22c769cc6b06e8b0a08c5924aae5", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a009e303767a28443e546ac8aab2539429f605e9acdc38bd43f3b13f1568bca9"},
- "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
- "finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"},
- "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
- "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
- "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
- "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
- "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
- "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
- "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
- "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
- "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
- "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
- "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
- "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
- "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
-}
diff --git a/shuffle/README.md b/shuffle/README.md
@@ -0,0 +1,25 @@
+# shuffle
+
+Picks random albums from a file of paths and adds them to the MPD queue via `mpc`.
+
+## Usage
+
+```sh
+shuffle <file> [-n <number>] [-d <depth>]
+```
+
+`<file>` should contain one MPD path per line (e.g. output of `mpc ls`).
+
+### Options
+
+| Flag | Default | Description |
+|------|---------|-------------|
+| `-n`, `--number` | `5` | Number of albums to pick |
+| `-d`, `--depth` | `2` | Minimum `/`-depth of path to include |
+
+### Example
+
+```sh
+mpc ls > albums.txt
+shuffle albums.txt -n 3
+```
diff --git a/speediness/README.md b/speediness/README.md
@@ -0,0 +1,25 @@
+# speediness
+
+A network speed test. Downloads 100 MB from OVH and uploads 20 MB to Cloudflare, printing live throughput and a final summary for each.
+
+## Building
+
+```sh
+zig build
+```
+
+## Usage
+
+```sh
+speediness
+```
+
+Output example:
+
+```
+ 100.0 MB 11.23 MB/s 89.8 Mbps
+100.0 MB in 8.90s => 11.23 MB/s (89.8 Mbps)
+
+ 20.0 MB 47.81 MB/s 382.5 Mbps
+20.0 MB in 0.42s => 47.81 MB/s (382.5 Mbps)
+```
diff --git a/tempfox/README.md b/tempfox/README.md
@@ -0,0 +1,22 @@
+# tempfox
+
+A Firefox [`user.js`](https://kb.mozillazine.org/User.js_file) with personal preference overrides.
+
+## Installation
+
+Copy `user.js` into your Firefox profile directory:
+
+```sh
+cp user.js ~/.mozilla/firefox/<profile>/user.js
+```
+
+Preferences are applied on the next Firefox launch. To make a preference permanent, it must remain in `user.js` — Firefox will not revert it automatically, but the GUI can override it until the next restart.
+
+## What it sets
+
+- Dark mode
+- Disables first-run and update pages
+- Disables default browser check
+- Disables `about:config` warning
+- Disables telemetry and data reporting notices
+- Disables the sidebar and vertical tabs
diff --git a/wlr-river-title/README.md b/wlr-river-title/README.md
@@ -0,0 +1,19 @@
+# wlr-river-title
+
+Prints the title of the currently focused view in the [River](https://codeberg.org/river/river) Wayland compositor and exits. Useful for feeding into status bars.
+
+## Building
+
+```sh
+make
+# or
+cc wlr-river-title.c river-status.c -o wlr-river-title $(pkg-config --cflags --libs wayland-client)
+```
+
+## Usage
+
+```sh
+wlr-river-title
+```
+
+Prints one line to stdout and exits. Exits with status 1 if the Wayland display cannot be connected to or the river-status protocol is unavailable.