Character sheet creation for the 1920s era call of cthulu characters
  • Rust 95.8%
  • Shell 2.1%
  • Nix 1.2%
  • TeX 0.7%
  • Just 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Agent 1ee97b73a5
fix(engine): reject 0d6 instead of silently rolling a single die
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>
2026-07-23 09:35:44 -04:00
.cargo build(nix): add a reproducible Nix build, toolchain pin, and SBOM 2026-07-20 16:57:25 -04:00
.cowork build(cowork): scaffold session-local agent identity 2026-07-21 16:26:17 -04:00
.forgejo/workflows ci(ci): add the security pipeline and SBOM/SLSA release 2026-07-20 16:57:25 -04:00
.githooks chore(repo): baseline import of coc workspace 2026-07-08 01:29:13 -04:00
ci ci(ci): add the security pipeline and SBOM/SLSA release 2026-07-20 16:57:25 -04:00
crates fix(engine): reject 0d6 instead of silently rolling a single die 2026-07-23 09:35:44 -04:00
docs docs(tui): sequences, macros, palette, and updated dispatch reference 2026-07-21 20:06:56 -04:00
fuzz build(security): add cargo-deny, clippy, lints, and a property/fuzz harness 2026-07-20 16:57:25 -04:00
scripts chore(repo): baseline import of coc workspace 2026-07-08 01:29:13 -04:00
xtask build(security): add cargo-deny, clippy, lints, and a property/fuzz harness 2026-07-20 16:57:25 -04:00
.gitignore build(cowork): scaffold session-local agent identity 2026-07-21 16:26:17 -04:00
Cargo.lock chore(api): remove unused serde_yaml dependency 2026-07-23 09:35:02 -04:00
Cargo.toml build(security): add cargo-deny, clippy, lints, and a property/fuzz harness 2026-07-20 16:57:25 -04:00
clippy.toml build(security): add cargo-deny, clippy, lints, and a property/fuzz harness 2026-07-20 16:57:25 -04:00
config.example.toml docs(tui): sequences, macros, palette, and updated dispatch reference 2026-07-21 20:06:56 -04:00
deny.toml fix(deps): bump ratatui to 0.30 to drop unsound/unmaintained transitives 2026-07-23 09:34:58 -04:00
flake.lock build(nix): commit flake.lock for reproducible builds 2026-07-23 09:35:06 -04:00
flake.nix build(nix): add a reproducible Nix build, toolchain pin, and SBOM 2026-07-20 16:57:25 -04:00
install.sh chore(repo): baseline import of coc workspace 2026-07-08 01:29:13 -04:00
justfile ci(api): fail the build when openapi.yaml drifts from the code 2026-07-08 01:29:13 -04:00
README.md docs(tui): sequences, macros, palette, and updated dispatch reference 2026-07-21 20:06:56 -04:00
rust-toolchain.toml build(nix): add a reproducible Nix build, toolchain pin, and SBOM 2026-07-20 16:57:25 -04:00
rustfmt.toml chore(repo): baseline import of coc workspace 2026-07-08 01:29:13 -04:00
SECURITY.md docs(docs): add the security policy, sources, and README section 2026-07-20 17:08:32 -04:00
sources.bib docs(docs): add the security policy, sources, and README section 2026-07-20 17:08:32 -04:00

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-auditable writes the crate graph into a .dep-v0 section during nix build ([profile.release] keeps strip = "none" so it survives); one syft pass over the binary's runtime closure (closureInfo in flake.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), a cargo fuzz target on the same oracle, Miri (nightly), Kani scaffolded off. The workspace forbids unsafe and denies clippy's all group; the restriction lints and pedantic are warn for 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×5 or (2D6+6)×5.
  • Derived attributes Hit Points (CON+SIZ)/10, Magic Points POW/5, Sanity = POW (cap 99 Cthulhu Mythos), Move rate (with age penalties), Damage Bonus / Build, and Dodge DEX/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 with h/j/k/l; j/k spill into the panel above/below at the edges. Enter rolls the selected entry:

    • a characteristic or skill → a d100 test popup showing the roll, the target, and the success level. In the popup: b bonus die, p penalty die, r reroll, Esc close.
    • a weapon → its damage dice (the damage bonus is applied automatically). Editing: e edits the selected value (characteristic, skill %, vital, or weapon name); n opens a picker to add a skill or a weapon; x deletes the selection; i edits 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. s saves. On the Skills panel, n offers to add a language (creating its own Language (…) 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 (so Language (Other) becomes Language (French)).

    Skill improvement (7e experience). Succeeding on a skill's d100 test auto-ticks it (shown as beside the name); space toggles a tick manually. Luck improves only when spent: spending it (Characteristics Luck row, -) ticks it, shown as on that row; space on that row also toggles the tick directly. At the end of a session press u to 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.

  • 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 with h/l; each ends in a + Add … row. Enter/e on 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), n adds, x deletes. 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/e edit an entry or money field, n add an entry to the current list section, x delete (or clear a money field).

  • Notes — a git log-style chronology of the investigator's notes. Enter opens the selected note in $EDITOR; n creates a new dated session note; t cycles the view density (compact / standard / comfy). From any tab, o opens 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/l to switch, j/k to select, Enter to activate, p to place an investigator into a campaign, n to 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/.