Template repository for rpg game character sheets.
  • Rust 88.5%
  • Shell 5.5%
  • Fluent 2.9%
  • Nix 1.9%
  • TeX 1.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Agent 95fa948d4a
All checks were successful
ci / gate (push) Successful in 1m39s
Template CI / lint (push) Successful in 1m3s
ci / deps (push) Has been skipped
ci / static (push) Has been skipped
ci / build (push) Has been skipped
Template CI / test (push) Successful in 6m26s
Template CI / nix-generate (push) Successful in 15m6s
fix(ci): handle directory argument in check-hygiene.sh
CI invokes the script with a single absolute directory path, which the
file-list branch treated as a literal filename and skipped (not a
regular file), making the hygiene gate pass vacuously with 0 files
checked. A single directory argument is now expanded via git ls-files
from that directory.
2026-07-23 09:22:21 -04:00
.cargo build(nix): add a reproducible Nix build, toolchain pin, and SBOM 2026-07-20 14:49:33 -04:00
.forgejo/workflows ci(ci): ship osv-scanner ignore list, fix opengrep PATH 2026-07-22 00:52:46 -04:00
.githooks build(hooks): install git pre-commit and pre-push hooks 2026-07-08 11:39:26 -04:00
ci ci(ci): add the security pipeline and SBOM/SLSA release 2026-07-20 14:49:54 -04:00
crates refactor(app): source RNG once in App::load, not per do_roll 2026-07-22 07:10:27 -04:00
fuzz test(security): add an engine property harness and fuzz target 2026-07-20 14:49:53 -04:00
scripts fix(ci): handle directory argument in check-hygiene.sh 2026-07-23 09:22:21 -04:00
.editorconfig chore(repo): scaffold cargo-generate character-sheet template 2026-07-08 11:37:10 -04:00
.gitignore chore(repo): ignore nix, fuzz, and release build outputs 2026-07-20 14:50:29 -04:00
AGENTS.md docs(docs): document the security and nix pipeline 2026-07-20 14:50:29 -04:00
cargo-generate.toml ci(ci): ship osv-scanner ignore list, fix opengrep PATH 2026-07-22 00:52:46 -04:00
Cargo.toml build(security): add cargo-deny, clippy, and workspace lints 2026-07-20 14:49:34 -04:00
clippy.toml build(security): add cargo-deny, clippy, and workspace lints 2026-07-20 14:49:34 -04:00
config.example.toml docs(tui): document chord grammar, sequences, macros, palette (Batch D) 2026-07-21 23:28:12 -04:00
CONTRIBUTING.md docs(repo): document commit conventions and hook workflow 2026-07-08 11:39:34 -04:00
deny.toml build(security): add cargo-deny, clippy, and workspace lints 2026-07-20 14:49:34 -04:00
flake.nix build(nix): add a reproducible Nix build, toolchain pin, and SBOM 2026-07-20 14:49:33 -04:00
install.sh chore(repo): scaffold cargo-generate character-sheet template 2026-07-08 11:37:10 -04:00
LICENSE chore(repo): scaffold cargo-generate character-sheet template 2026-07-08 11:37:10 -04:00
openapi.yaml chore(repo): scaffold cargo-generate character-sheet template 2026-07-08 11:37:10 -04:00
osv-scanner.toml ci(ci): ship osv-scanner ignore list, fix opengrep PATH 2026-07-22 00:52:46 -04:00
README.md docs(tui): document chord grammar, sequences, macros, palette (Batch D) 2026-07-21 23:28:12 -04:00
rust-toolchain.toml build(nix): add a reproducible Nix build, toolchain pin, and SBOM 2026-07-20 14:49:33 -04:00
rustfmt.toml chore(repo): scaffold cargo-generate character-sheet template 2026-07-08 11:37:10 -04:00
SCOPES.md docs(docs): document the security and nix pipeline 2026-07-20 14:50:29 -04:00
SECURITY.md docs(security): add the security policy and source bibliography 2026-07-20 14:50:10 -04:00
sources.bib docs(security): add the security policy and source bibliography 2026-07-20 14:50:10 -04:00
TAGS.md docs(repo): document commit conventions and hook workflow 2026-07-08 11:39:34 -04:00

{{project-name}}

Tools for running a {{system_name}} character: a shared core library, a command-line manager ({{project-name}}), and a full-screen ratatui TUI ({{project-name}}-tui), all over an XDG-backed store of YAML sheets. The skeleton compiles, its tests pass, and CI is green; the game-specific content is yours to fill in -- and most of it lives in one data file, not in Rust.

This project was generated from a cargo-generate template. Create a new one with:

cargo generate --git {{repository}} --name my-rpg

What makes it a template

The model is data-driven. Abilities, skills, resource pools, inventory and backstory are all lists on the sheet, and character creation is defined by crates/core/data/creation.yaml. To model a different system you edit that YAML -- rename the abilities, add skills, define starting archetypes -- with no changes to the Rust types. Character sheets themselves are YAML, read at runtime from the store; the rules most specific to a system live in the small rules.rs, ready to be swapped.

Workspace layout

A Cargo workspace (edition 2021) under crates/:

  • crates/core ({{project-name}}-core) -- the shared library: the [Sheet] model (serde-mapped to characters/<slug>.yaml), a dice-expression evaluator, the XDG store (characters, campaigns, per-character notes), config, the system-agnostic rules helpers, the creation tables loaded from data/creation.yaml, and entries/search -- the addressable enumeration of a sheet's rows that the TUI's finder is built on.
  • crates/cli ({{project-name}}) -- a thin command-line client over the store.
  • crates/tui ({{project-name}}-tui) -- a five-tab terminal manager (Sheet, Inventory, Backstory, Notes, Meta) with a configurable theme and keybindings.

Quick start

cargo build --workspace
cargo test --workspace
cargo run -p {{project-name}}-cli -- init                          # scaffold data + config
cargo run -p {{project-name}}-cli -- new --auto --archetype warrior  # roll a character
cargo run -p {{project-name}}-cli -- list
cargo run -p {{project-name}}-cli -- show
cargo run -p {{project-name}}-tui                                  # open the active sheet

Storage

Characters live in an XDG data directory (default $XDG_DATA_HOME/{{project-name}}/, i.e. ~/.local/share/{{project-name}}/):

characters/<slug>.yaml      one sheet per character
ACTIVE_CHARACTER            symlink -> the active character's sheet
campaign/                   campaigns container
  ACTIVE_CAMPAIGN           symlink -> the active campaign directory
  <name>/notes/<slug>/      threads.md, ledger.md, session-*.md

Configuration is read from $XDG_CONFIG_HOME/{{project-name}}/config.toml (see config.example.toml), including the TUI's [theme], [keys] and [log] tables.

CLI -- {{project-name}}

command what it does
init scaffold the data + config directories
list list stored characters (* marks active)
show [slug] show a character (default: active)
use <slug> set the active character
import <path> import a sheet file and activate it
archetypes list creatable archetypes
roll <expr> dice roller, e.g. roll "3d6" or roll "d20 + 4"
new [--archetype <id>] [--name <name>] interactive step-by-step creation
new --auto --archetype <id> roll a whole character in one shot
campaign list | new <name> | use <name> manage campaigns
deps [--refresh] what this build is made of: every crate, its known advisories and its source repo's activity score

With no TTY (piped input) every creation prompt accepts its roll, so printf '' | {{project-name}} new --archetype warrior rolls a full character.

TUI -- {{project-name}}-tui

# First run: pass a sheet to import into the store and open it.
cargo run -p {{project-name}}-tui -- ./someone.yaml
# Later: no argument opens the active character.
cargo run -p {{project-name}}-tui

Five tabs, cycled with Tab / Shift-Tab:

  • Sheet -- Abilities, Skills and Resources panels (h/l switch panel, j/k move). Enter rolls the selection (a d20 + the ability/skill); e edits a value; +/- adjust a resource pool. In the roll popup, r rerolls and Esc closes.
  • Inventory -- n add, e rename, x delete, +/- change quantity.
  • Backstory -- sections on the left (h/l), entries on the right; n add, e edit, x delete.
  • Notes -- a chronology of the character's notes; Enter/e opens one in $EDITOR, n creates a new dated session note, t cycles view density.
  • Meta -- switch the active character or campaign (h/l, j/k, Enter), and p places the character's notes into the selected campaign.

/ opens the finder: type to fuzzy-search the sheet, Up/Down move through the hits, Enter jumps to the row -- on whichever tab holds it, ready to roll or edit -- and Esc cancels. It offers rollable rows (abilities and skills) by default; Tab widens it to every row. What it can find is core::entries, the same enumeration the CLI and the API see, so the finder cannot drift out of step with what the sheet actually holds.

d opens the build-dependency page: every crate this build ships, its known CVEs and the OpenSSF Scorecard of its source repository (the activity score), with the age of that health stated on the page. The crate list is captured at build time from cargo metadata and is always there; the health is looked up from deps.dev, which is often unreachable at build time -- then every cell reads ? until {{project-name}} deps --refresh fills it in. A ? is never a 0: not knowing is not the same as finding nothing.

? shows help, s saves, g edits the config in $EDITOR (reloads on save), q quits. Colours and every keybinding are configurable -- see config.example.toml.

Modifier chords, sequences, macros, palette. Bindings accept the kakoune-style c-/a-/s- prefix grammar (c-j, a-x, c-a-t). Defaults add c-h/j/k/l for panel focus (no spill), c-u/c-d for half-page scroll, a-1..a-5 for jump-to-tab, c-e to drop $EDITOR on the newest note, and c-p to open the action palette -- a fuzzy overlay listing every built-in action and every user macro with its current bind, Enter to run.

Two-key sequences live under [keys.sequences] ("g n" = "goto-tab-notes"); pressing the prefix parks a pending state (status shows [g-…]), the second chord completes it, Esc cancels. Depth capped at 2. User macros in [macros] are ordered lists of action names, bound via [keys.sequences] or [keys.macro_binds]. Macros that reference other macros are refused at load (vim's noremap lesson).

Where the terminal supports it, the TUI opts into the kitty keyboard protocol (DISAMBIGUATE_ESCAPE_CODES) so c-i/c-m don't collide with Tab/Enter and a- chords are reliably distinct from a lone ESC. On legacy terminals the a- and c-<digit> chords may not be delivered; the config's default set sticks to combinations that work everywhere.

Internationalization

The user interface is localized with Fluent. UI strings (CLI output, TUI labels, prompts, help) are looked up by id from per-locale catalogues under crates/core/i18n/ (en.ftl, fr.ftl); en is the base and fallback. Game content -- ability, skill and archetype names in data/creation.yaml -- stays English and is not translated.

Pick a language via config.toml (locale = "fr") or the environment (LANG, LC_ALL, ...), e.g.:

LANG=fr_FR.UTF-8 {{project-name}} show

Add a language by copying crates/core/i18n/en.ftl to i18n/<code>.ftl, translating the values, and adding the (code, include_str!) entry to LOCALES in crates/core/src/i18n.rs. A test there checks that every id resolves in every bundled locale. (clap's own --help text stays English.)

API contract (api + spec)

The CLI exposes its operations as a small HTTP-shaped contract so other tools (an editor plugin, a script) can drive it without a server. api handles one request -- a method, a path, and an optional JSON body -- and prints the JSON response, exiting non-zero on a 4xx/5xx status:

{{project-name}} api GET  /characters
{{project-name}} api POST /characters --body '{"archetype":"warrior","name":"Bran"}'
echo '{"expr":"d20 + 4"}' | {{project-name}} api POST /dice/roll
{{project-name}} api GET  /system        # the whole game system (abilities, skills, archetypes)

Routes: /characters, /characters/{slug}, /characters/{slug}/active, /active, /import, /archetypes, /system, /campaigns, /campaigns/{name}/active, /dice/roll.

The matching OpenAPI 3.1 document is generated from the Rust types (so it can't drift) and committed at openapi.yaml. Regenerate it with:

{{project-name}} spec --format yaml > openapi.yaml   # or: --format json

The schemas and routing live in crates/core/src/api/; the document in crates/core/src/openapi.rs, behind the core openapi feature (which the CLI enables). A test keeps openapi.yaml in sync with the code.

Security & supply chain

The build and CI are aligned with the rust-ci-template: the release artifact is built by Nix, its SBOM is generated at build time rather than scanned off the finished binary, and the provenance is signed and independently checkable. The honest summary is that this reaches SLSA Build L2 in form (signed provenance from a hosted runner) but not L3 (the cosign key is readable by the release job); SECURITY.md states the gap precisely and is the process half of CRA compliance.

What you get out of the box:

  • One-pass CycloneDX 1.6 SBOM. cargo-auditable writes the crate graph into a .dep-v0 linker section during nix build ([profile.release] keeps strip = "none" so it survives). A single syft pass over the binary's runtime closure (closureInfo in flake.nix) then catalogs both the native/system store packages and the Rust crates from that section — one tool, both halves, no merge.
  • Code scanning on every PR (.forgejo/workflows/ci.yml): cargo audit, cargo deny, osv-scanner, cargo machete, cargo geiger, plus rustfmt, clippy, and opengrep.
  • Dynamic + static analysis: proptest properties over the engine primitives (crates/engine/src/props.rs), a cargo fuzz target sharing the same oracle, Miri (nightly), and Kani scaffolded off by default. The workspace forbids unsafe and denies the clippy all group; the restriction lints (unwrap/expect/panic/indexing) and pedantic are warn for now, to be tightened to deny per crate.
  • Reproducibility enforced at release time (nix build --rebuild), signed tags required, and every SBOM pushed to Dependency-Track for continuous re-evaluation.

Run the PR gate locally (inside nix develop, or with the tools on your PATH):

cargo ci                       # fmt, clippy, test, deny, audit
nix build .#sbom && jq . result   # the CycloneDX SBOM (native + Rust)
nix build .#cli                # the auditable release binary

The pipeline assumes a Forgejo runner labelled docker running the nixos/nix image. The flake, workflows and scripts were validated by review and parsing, not by a live Nix/Forgejo run — treat your first nix build and first tagged release as the real test. Note that nix build and the release pipeline need Cargo.lock and flake.lock committed first (a fresh generated project has neither until you run cargo build and nix flake lock).

Install

./install.sh          # cargo install both binaries, then `{{project-name}} init`

Start with AGENTS.md for the conventions and the "add a new system" workflow.

License

Released under the MIT license.