- Rust 95.8%
- Shell 2.1%
- Nix 1.2%
- TeX 0.7%
- Just 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Port the upstream character-sheet-template fix (889e104): the vendored copy here still normalized Tok::Dice(0, _) via count.max(1), so a zero-dice expression like 0d6 silently rolled one die instead of reporting the typo. Bail with the same message the template uses and add 0d6 to error_paths regression coverage. Assisted-by: Agent (claude) <ai@blobfish.icu> |
||
| .cargo | ||
| .cowork | ||
| .forgejo/workflows | ||
| .githooks | ||
| ci | ||
| crates | ||
| docs | ||
| fuzz | ||
| scripts | ||
| xtask | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| clippy.toml | ||
| config.example.toml | ||
| deny.toml | ||
| flake.lock | ||
| flake.nix | ||
| install.sh | ||
| justfile | ||
| README.md | ||
| rust-toolchain.toml | ||
| rustfmt.toml | ||
| SECURITY.md | ||
| sources.bib | ||
coc
A Cargo workspace of tools for running a Call of Cthulhu (7th edition)
investigator — a sibling to the pirate-borg tool one directory over, sharing
its shape: a core library, a full-screen TUI, and a CLI over an XDG-backed store.
crates/core shared library: model, dice, rules, XDG store, config, creation tables
crates/tui coc-tui — full-screen terminal manager
crates/cli coc — command-line manager over the investigator set
Storage
Investigators live in an XDG data directory (default
$XDG_DATA_HOME/coc/, i.e. ~/.local/share/coc/):
investigators/<slug>.yaml one sheet per investigator
ACTIVE_INVESTIGATOR symlink -> the active investigator's sheet
ACTIVE_CAMPAIGN symlink -> the active campaign directory
campaign/<name>/notes/<slug>/ per-investigator notes within a campaign
Configuration is read from $XDG_CONFIG_HOME/coc/config.toml
(see config.example.toml), including an optional campaign_dir that points
the TUI's Notes tab at a repo of your choosing.
Sheets carry a schema_version. Older sheets are upgraded automatically when
opened in the TUI or imported (missing derived attributes are backfilled and new
fields are added without losing data); coc migrate upgrades every stored sheet
in place in one pass.
Opening a sheet that is already at the current schema_version does not write
to it. Top-level keys the tool does not model -- GM annotations, house rules --
are preserved across a load/edit/save cycle, and saves are atomic (written to a
temporary file and renamed), so an interrupted write cannot truncate a sheet.
Security & supply chain
The build and CI are aligned with character-sheet-template: the release artifact
is built by Nix, its SBOM is generated from the build rather than scanned off a
finished binary, and the provenance is signed and independently checkable. 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 and
is the process half of CRA compliance.
Out of the box:
- One-pass CycloneDX 1.6 SBOM.
cargo-auditablewrites the crate graph into a.dep-v0section duringnix build([profile.release]keepsstrip = "none"so it survives); onesyftpass over the binary's runtime closure (closureInfoinflake.nix) then catalogs both the native store packages and the Rust crates — one tool, both halves. - Code scanning every PR (
.forgejo/workflows/ci.yml): cargo-audit, cargo-deny, osv-scanner, cargo-machete, cargo-geiger, rustfmt, clippy, opengrep. - Dynamic + static analysis: proptest properties over the engine primitives
(
crates/engine/src/props.rs), acargo fuzztarget on the same oracle, Miri (nightly), Kani scaffolded off. The workspace forbidsunsafeand denies clippy'sallgroup; the restriction lints andpedanticarewarnfor now. - Reproducibility enforced at release (
nix build --rebuild), signed tags required, and every SBOM pushed to Dependency-Track.
cargo ci # fmt, clippy, test, deny, audit
nix build .#sbom && jq . result # the CycloneDX SBOM (native + Rust)
nix build .#cli # the auditable release binary
CI runs on a Forgejo runner labelled default: each job runs in a Node container
and installs Lix (ci/install-nix.sh). nix build and the release pipeline need
Cargo.lock and flake.lock committed.
Install
cd coc
./install.sh # cargo install both binaries, then `coc init`
install.sh builds and installs coc and coc-tui to ~/.cargo/bin, then
runs coc init to scaffold the data directory (investigators/, a default
campaign) and write a documented config.toml. To build without installing:
cargo build # or: cargo build --release
cargo test
The rules it implements
7e investigator creation and play mechanics, drawn from the rulebook pages under
../character-creation/:
- Characteristics STR, CON, SIZ, DEX, APP, INT, POW, EDU (percentile) plus
Luck, rolled
3D6×5or(2D6+6)×5. - Derived attributes Hit Points
(CON+SIZ)/10, Magic PointsPOW/5, Sanity= POW(cap99 − Cthulhu Mythos), Move rate (with age penalties), Damage Bonus / Build, and DodgeDEX/2. - Skills the full standard skill list with base values; occupation skills and
personal-interest points (
INT×2) allocated at creation. - Occupations the core-rulebook set (Antiquarian through Zealot) with their skill-point formulas, Credit Rating bands and skill lists. Specialisms are simplified to their generic skill; consult the book for period specifics.
- Resolution d100 roll-under with the 7e success levels (Fumble / Failure / Regular / Hard / Extreme, plus a natural 01 Critical) and bonus/penalty dice.
- Age modifiers, backstory tables, and a Credit-Rating cash/assets table.
TUI — coc-tui
# First run: pass a sheet to import into the store and open it.
cargo run -p coc-tui -- ../someone.yaml
# Later: no argument opens the active investigator.
cargo run -p coc-tui
Five tabs, cycled with Tab:
-
Sheet — a 2x2 grid: Characteristics (including Luck), Skills (scrollable, occupation skills marked
◆), Weapons, and Vitals (HP / MP / Sanity). Move focus withh/j/k/l;j/kspill into the panel above/below at the edges.Enterrolls the selected entry:- a characteristic or skill → a d100 test popup showing the roll, the
target, and the success level. In the popup:
bbonus die,ppenalty die,rreroll,Escclose. - a weapon → its damage dice (the damage bonus is applied automatically).
Editing:
eedits the selected value (characteristic, skill %, vital, or weapon name);nopens a picker to add a skill or a weapon;xdeletes the selection;iedits investigator info (name, occupation, age, …). On the Vitals panel,+/-adjust the selected pool. Luck lives once, in the Characteristics panel:+/-on its row spend and regain it. A full-width funds bar along the bottom always shows Cash / Assets / Spending level.ssaves. On the Skills panel,noffers to add a language (creating its ownLanguage (…)skill at 1%, so you can track several), to add a custom skill, or — when a specialisable skill is selected (e.g.Language (Other),Science,Art/Craft) — to specialise it in place, naming the language or field while keeping its rolled value (soLanguage (Other)becomesLanguage (French)).
Skill improvement (7e experience). Succeeding on a skill's d100 test auto-ticks it (shown as
✓beside the name);spacetoggles a tick manually. Luck improves only when spent: spending it (Characteristics Luck row,-) ticks it, shown as✓on that row;spaceon that row also toggles the tick directly. At the end of a session pressuto run the improvement rolls: each ticked skill (and Luck, if it was spent) rolls 1D100 and, if the roll beats its current value (or beats 95), gains 1D10 points; a scrollable report shows the before/after and skills crossing 90%, then the ticks are cleared. Cthulhu Mythos and Credit Rating are never improvable this way. - a characteristic or skill → a d100 test popup showing the roll, the
target, and the success level. In the popup:
-
Spells — Mythos spells and an ingredient glossary. A magic-points gauge runs across the top;
+/-spend and regain MP directly from this screen. The left sidebar holds two lists — Mythos Spells and the Ingredient Glossary — switched withh/l; each ends in a+ Add …row.Enter/eon an entry opens a field editor (a spell's cost, sanity cost, casting time, ingredients, incantation, special circumstances and notes; an ingredient's source, other uses, relative value and notes),nadds,xdeletes. In a spell's detail pane, ingredients also present in the glossary are marked◆. -
Backstory — the ten 7e backstory sections (Personal Description, Ideology, Significant People, …), followed by editable subpanels for Funds & Assets (Cash / Assets / Spending level), Inventory (carried equipment), and Fellow Investigators.
Enter/eedit an entry or money field,nadd an entry to the current list section,xdelete (or clear a money field). -
Notes — a
git log-style chronology of the investigator's notes.Enteropens the selected note in$EDITOR;ncreates a new dated session note;tcycles the view density (compact/standard/comfy). From any tab,oopens a one-line prompt that appends a bullet to the newest session note (creating one if needed) — a quick way to jot something mid-play without leaving the sheet. -
Meta — manage investigators and campaigns: two panels,
h/lto switch,j/kto select,Enterto activate,pto place an investigator into a campaign,nto create a campaign. The active ones are marked*.
From any tab, / opens a fuzzy finder (telescope-style) over every Sheet
entry — characteristics, skills, weapons and vitals. Type to filter, c-j/c-k
or arrows to move, Enter to jump focus straight to that entry on the Sheet
tab (ready to roll or edit), Esc to cancel. The first five results carry
[a-1]..[a-5] chord chips; pressing the chord selects that row and fires
the jump in one press.
Press ? for a full help panel, g to edit the config file in $EDITOR
(settings reload on save), and c for the campaign switcher. Colours and every
keybinding are configurable via config.toml — see config.example.toml.
Modifier chords. Every binding accepts the kakoune-style c-/a-/s-
prefix grammar (c-j, a-x, c-a-t). Chained prefixes may appear in any
order; each may appear only once. s-<char> is refused — a shifted printable
is already its own char. The defaults add several modifier chords: c-h/j/k/l
focus the panel left/down/up/right without spilling through in-panel rows
(the fix for j-walking a hundred Skills to reach Vitals); c-u/c-d move
the selection up/down by half a page inside the focused panel; a-1..a-6
jump straight to a tab (Sheet, Spells, Backstory, Notes, Meta, SBOM); c-e
drops into $EDITOR on the newest session note; c-p opens the action
palette. c-i and c-m are avoided in defaults: legacy terminal encodings
can't tell them from Tab and Enter. a- chords and c-<digit> need a
terminal that reports proper alt modifiers (kitty / foot / iTerm2 do; xterm
often does not).
Two-key sequences. Bindings under [keys.sequences] name a two-chord
gesture: pressing the prefix parks a pending state (the status bar shows
[g-…]); the second chord completes the binding, Esc cancels, and any
unmatched second chord cancels with a "no binding for X Y" message. Depth
is capped at 2. Sequences take priority over single-chord binds, so g n
and single g (default configure) coexist by turning g into a prefix
in that config.
Macros and the action palette. Define named macros in [macros] — each
is an ordered list of built-in action names (kebab-case: goto-tab-notes,
edit, save, …). Bind them to a chord via [keys.macro_binds] or to a
two-key sequence via [keys.sequences]; the right-hand string names an
action or a macro interchangeably. Macros that reference other macros are
refused at load (vim's noremap lesson); a macro whose step is an unknown
action halts on that step with a status. c-p opens the action palette:
a fuzzy overlay listing every built-in action and every user macro with
its current bind, Enter to run.
CLI — coc
cargo run -p coc-cli -- <command>
| command | what it does |
|---|---|
init |
scaffold the data + config directories |
list |
list stored investigators (* marks active) |
show [slug] |
show an investigator (default: active) |
use <slug> |
set the active investigator |
import <path> |
import a sheet file and activate it |
occupations |
list creatable occupations |
migrate |
upgrade all stored sheets to the current schema |
roll <expr> |
dice roller, e.g. roll "3d6 x 5" or roll 1d100 |
new [--occupation <id>] [--name <name>] [--age <n>] |
interactive step-by-step creation |
new --auto --occupation <id> |
roll a whole investigator 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 |
Creation
coc new rolls the eight characteristics + Luck (Enter accepts, r rerolls, or
type a value), then takes an occupation and age, allocates occupation and
personal-interest skill points, sets Credit Rating within the occupation's band,
rolls a backstory, and derives every attribute. With no TTY (piped input) every
prompt accepts its roll, so printf '' | coc new --occupation author rolls a
full investigator; coc new --auto --occupation author does the same directly.
The creation tables in crates/core/data/creation.yaml are transcribed from the
rulebook pages in ../character-creation/.