7.0 KiB · text History 832396d

bismuth

CI status license install bismuth

Code screenshots with Linux window chrome, in the spirit of carbon.now.sh. Paste code, pick a KDE-era window decoration and a color scheme, then download a PNG or SVG. Or save it and embed the live card in a presentato deck, where it follows the deck’s color scheme the way Diogramos embeds do.

Built on the neiam-htmx cargo-generate template: axum + htmx + minijinja, sea-orm on Postgres, and invite-only session auth.

The card

Choice Options
Window decoration KDE 1 (kwm), KDE 2, KDE 3 Keramik, KDE 3.5 Plastik, KDE 4 Oxygen, Plasma Breeze, none
Color scheme Auto (the decoration’s own era), the KDE classics (KDE 1, KDE 2, Keramik, Plastik, Oxygen, Obsidian Coast, Breeze, Breeze Light, Breeze Dark), and the neiam schemes (her, afterdark, forest, sky, clays, stones, light, dark, lofi, black, blueprint)
Highlighting Solarized by default, following the scheme: dark or light to match it. On a neiam scheme the code area also takes that scheme’s own base-100 fill and text color, keeping Solarized’s accents. Fixed themes (Solarized dark/light, Base16 Ocean/Eighties/Mocha, InspiredGitHub) keep their own background.
Extras line numbers, transparent backdrop, padding

The decorations are vector redraws that follow each original’s layout: button placement, caption alignment, bevels, Keramik’s raised caption tab, KDE 2’s grab bar. They aren’t copies of KDE pixmaps. The Breeze color values come straight from Plasma’s .colors files. The KDE 1–4 sets are reconstructions of each era’s defaults.

Everything renders through one path (src/render/): syntect highlights, the card is composed as SVG, and the PNG export rasterises that same SVG with resvg. The house B612 fonts are compiled in (fonts/, OFL-1.1), so exports look the same on any host.

Editing

The editor (/) shows the rendered window and lets you type straight into it. A transparent, caret-only textarea sits exactly over the code grid, and each keystroke re-renders the card underneath. The plain text box is hidden behind Show plain text box for long pastes; the two stay in sync.

Sharing and embedding

Saving (signed in) gives an unlisted random slug. Anyone with the link can:

URL What
/s/<slug> share page with copy-paste snippets
/s/<slug>/card.png PNG (2x by default)
/s/<slug>/card.svg standalone SVG, fonts embedded
/e/<slug> iframe embed, frameable anywhere (frame-ancestors *)

All four accept look overrides, so one snippet can be dressed per placement: ?theme=<palette>, ?chrome=, ?hl=, ?bg=transparent|backdrop, ?pad=, ?ln=true|false, plus ?scale= (PNG) and ?download=1.

Presentato

The embed uses the same theme handshake as Diogramos, under its own prefix:

  • the embed posts {type: "bismuth:ready"} to its parent once it’s listening
  • the parent posts {type: "bismuth:set-theme", theme}, and the card is re-rendered in that palette and swapped in

Every presentato scheme name is a bismuth palette. presentato-ma and presentato-gmp-io speak both prefixes. In a deck:

<iframe src="https://bismuth.example/e/<slug>?bg=transparent"
        style="width:100%;height:540px;border:0;color-scheme:normal" loading="lazy"></iframe>

color-scheme:normal matters: the decks declare color-scheme: dark, and browsers paint an opaque backdrop behind an iframe whose scheme differs from its parent’s.

Stack

Concern Choice
HTTP axum 0.8
Hypermedia htmx (hx-boost, polling), axum-htmx
Templating minijinja (embedded at build time)
Database Postgres via sea-orm (UUIDv7 keys)
Migrations sea-orm-migration (auto-run on boot)
Auth Argon2 password hashing, cookie sessions, invite tokens
Styling Tailwind CSS + DaisyUI + FontAwesome
Metrics Prometheus at /metrics

Binaries

  • webservice — the HTTP server.
  • registration-token — CLI to mint/list invite tokens (bootstrap the first user).

Quick start (local dev)

Prereqs: Rust, just, Node.js (for the asset build), and Docker/Podman (for Postgres).

# 1. Build front-end assets (CSS/JS/webfonts)
just deps
just assets

# 2. Start Postgres
just infra

# 3. Run the web server (auto-runs migrations on boot)
just serve                     # -> http://localhost:3999

# 4. Mint an invite so you can register, then open the printed URL
just invite

Registration is invite-only by default. Set REGISTRATION_ENABLED=true to let anyone with a valid invite token self-serve, or keep it off and hand out links from the in-app Invites page.

Configuration

All via environment variables (defaults suit local compose.yml):

Variable Default Purpose
DATABASE_URL postgres://app_user:app_password@localhost:5433/app Postgres DSN
BIND_ADDRESS 0.0.0.0:3999 Listen address
AUTO_MIGRATE true Run migrations on webservice boot
REGISTRATION_ENABLED false Allow public self-registration
SESSION_DURATION_HOURS 24 Session lifetime
REGISTRATION_TOKEN_DURATION_DAYS 7 Default invite lifetime
PUBLIC_URL http://localhost:3999 Origin used in the copy-paste embed/image snippets

Containers

just up      # build + run postgres, webservice
just logs    # tail the webservice
just invite-in-pod
just down

Layout

src/
  bin/            webservice, registration-token
  config.rs       env-driven configuration
  lib.rs          AppState + minijinja environment
  entities/       sea-orm models (users, sessions, invites, snippets)
  auth/           password hashing, sessions, invite tokens, user ops
  middleware/     cookie-session auth layers
  handlers/       axum handlers (auth, editor/export, snippets/embeds)
  render/         highlight → SVG card → PNG; chrome.rs (decorations),
                  palette.rs (color schemes), highlight.rs (syntect)
fonts/            B612 TTFs for rasterising (OFL-1.1)
examples/         contact_sheet: every decoration and palette to PNG/SVG
migration/        sea-orm migrations
templates/        minijinja templates (excluded from generation templating)
assets/           Tailwind/esbuild sources + vendored fonts
tests/            template render smoke tests

Regenerating entities after a schema change

sea-orm-cli generate entity -u "$DATABASE_URL" -o src/entities