- Rust 88.5%
- Shell 5.5%
- Fluent 2.9%
- Nix 1.9%
- TeX 1.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
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
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. |
||
| .cargo | ||
| .forgejo/workflows | ||
| .githooks | ||
| ci | ||
| crates | ||
| fuzz | ||
| scripts | ||
| .editorconfig | ||
| .gitignore | ||
| AGENTS.md | ||
| cargo-generate.toml | ||
| Cargo.toml | ||
| clippy.toml | ||
| config.example.toml | ||
| CONTRIBUTING.md | ||
| deny.toml | ||
| flake.nix | ||
| install.sh | ||
| LICENSE | ||
| openapi.yaml | ||
| osv-scanner.toml | ||
| README.md | ||
| rust-toolchain.toml | ||
| rustfmt.toml | ||
| SCOPES.md | ||
| SECURITY.md | ||
| sources.bib | ||
| TAGS.md | ||
{{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 tocharacters/<slug>.yaml), a dice-expression evaluator, the XDG store (characters, campaigns, per-character notes), config, the system-agnosticruleshelpers, thecreationtables loaded fromdata/creation.yaml, andentries/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/lswitch panel,j/kmove).Enterrolls the selection (ad20 +the ability/skill);eedits a value;+/-adjust a resource pool. In the roll popup,rrerolls andEsccloses. - Inventory --
nadd,erename,xdelete,+/-change quantity. - Backstory -- sections on the left (
h/l), entries on the right;nadd,eedit,xdelete. - Notes -- a chronology of the character's notes;
Enter/eopens one in$EDITOR,ncreates a new dated session note,tcycles view density. - Meta -- switch the active character or campaign (
h/l,j/k,Enter), andpplaces 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-auditablewrites the crate graph into a.dep-v0linker section duringnix build([profile.release]keepsstrip = "none"so it survives). A singlesyftpass over the binary's runtime closure (closureInfoinflake.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, plusrustfmt,clippy, andopengrep. - Dynamic + static analysis: proptest properties over the engine primitives
(
crates/engine/src/props.rs), acargo fuzztarget sharing the same oracle, Miri (nightly), and Kani scaffolded off by default. The workspace forbidsunsafeand denies the clippyallgroup; the restriction lints (unwrap/expect/panic/indexing) andpedanticarewarnfor now, to be tightened todenyper 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`
Where to read next
Start with AGENTS.md for the conventions and the "add a new system" workflow.
License
Released under the MIT license.