About: contributor and model credits compiled in (grab bag + table, self-submitted names) (#898)

* About: contributors and models tabs compiled in from contributors.json (grab bag + table, sortable, name toggle)

Help > About PhotoCraft gets tabs About · Contributors · Models, following the craftrules
standard (standards/contributors.md): contributors/contributors.json is generated by
craftrules/scripts/contributors.py and baked into the binary by crates/ui-egui/build.rs as
static tables (nothing read at run time; the web build has them too). Each contributor line:
@username, opt-in display/real name, PRs, commits, lines +/−/Δ, binary assets +/−, first/last
commit. Sortable by every column; alphabetical ignores case and the leading @. The tab is the
dialog field `tab`, so agents can switch it with ui.dialog.set. New labels are translated in
all eleven catalogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Contributors docs: craftrules is two levels up (../../craftrules)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Contributors: self-submitted names in contributors/people.toml; AGENTS.md credit-yourself section

build.rs reads contributors/people.toml next to the generated contributors.json and shows a name only
for entries the generator verified as written by that person (craftrules standards/contributors.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Contributors: regenerate (verified people.toml entries)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n: credits labels for catalogs added on main since this branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Contributors: regenerate after rebase onto main

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Contributors: regenerate (verified people.toml entries)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-07 21:22:11 -04:00 committed by GitHub
parent aaa946b56d
commit 44303db546
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1979 additions and 9 deletions

View File

@ -131,3 +131,21 @@ Then append a terse entry to `log/devlog.md` (what landed, numbers, what's still
nothing is silently dropped from `.pcraft` saves. When you add a doc field, add it to
`crates/format/src/manifest.rs` and `convert.rs` with `#[serde(default)]` so older files still load.
If the field has a PSD equivalent, map it in `crates/io` too, and keep unknown PSD blocks verbatim.
## Contributor credits (About window)
- About ▸ Contributors/Models are compiled into the binary from `contributors/contributors.json`
(commit stats; generated, never hand-edit) and `contributors/people.toml` (names people chose for
themselves). See `docs/contributors.md`.
- **Agents working for a contributor:** when you prepare a PR, check whether your human's GitHub
username has a `[people.<username>]` entry in `contributors/people.toml`. If not, ask them once
whether they want to be credited by more than their username: a real name, a display name, and/or
their public GitHub profile name (`sync_github_name = true`). If yes, add **only their own** entry
(copy the template at the top of the file, or run
`python3 ../../craftrules/scripts/contributors.py --add-me . --real-name "…" --sync-github-name`)
and include it in their PR, committed as them. If no, change nothing: they are credited as
`@username` anyway.
- Never add, edit, guess or copy anyone else's entry or name (not from git config, commit authors or
GitHub profiles). Never hand-edit `contributors.json`.
- Maintainers refresh the stats with `python3 ../../craftrules/scripts/contributors.py .` (it also
re-verifies who wrote each `people.toml` entry; `--check` only verifies).

1
Cargo.lock generated
View File

@ -3795,6 +3795,7 @@ dependencies = [
"serde",
"serde_json",
"sys-locale",
"toml",
]
[[package]]

View File

@ -0,0 +1,943 @@
{
"schema": 1,
"repo": "storytold/photocraft",
"branch": "main",
"head": "aaa946b56df2d74d198c47e67dda35ddce0b3769",
"head_date": "2026-10-08T01:20:47Z",
"commits": 482,
"unattributed_commits": 0,
"contributors": [
{
"login": "echelon",
"people_entry": true,
"github_name": null,
"prs": 124,
"commits": 201,
"lines_added": 277845,
"lines_deleted": 22466,
"binary_added": 39,
"binary_deleted": 8,
"first_commit": "2026-09-30T06:12:04Z",
"last_commit": "2026-10-07T22:09:41Z"
},
{
"login": "storybored379",
"people_entry": false,
"github_name": null,
"prs": 8,
"commits": 8,
"lines_added": 2707,
"lines_deleted": 54,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-04T04:57:38Z",
"last_commit": "2026-10-05T22:14:20Z"
},
{
"login": "Multipad-cyber",
"people_entry": false,
"github_name": null,
"prs": 3,
"commits": 3,
"lines_added": 171,
"lines_deleted": 27,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-04T05:02:49Z",
"last_commit": "2026-10-06T11:43:37Z"
},
{
"login": "torstfugl",
"people_entry": false,
"github_name": null,
"prs": 3,
"commits": 3,
"lines_added": 377,
"lines_deleted": 8,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-04T05:03:49Z",
"last_commit": "2026-10-04T16:21:25Z"
},
{
"login": "sommerper",
"people_entry": false,
"github_name": null,
"prs": 9,
"commits": 9,
"lines_added": 865,
"lines_deleted": 148,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-04T14:13:31Z",
"last_commit": "2026-10-07T11:12:58Z"
},
{
"login": "Gh0stlyKn1ght",
"people_entry": false,
"github_name": null,
"prs": 4,
"commits": 4,
"lines_added": 3349,
"lines_deleted": 255,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-04T16:31:18Z",
"last_commit": "2026-10-05T05:02:40Z"
},
{
"login": "MikeeI",
"people_entry": false,
"github_name": null,
"prs": 22,
"commits": 22,
"lines_added": 753,
"lines_deleted": 178,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-05T04:59:38Z",
"last_commit": "2026-10-05T05:37:25Z"
},
{
"login": "tiredIsa",
"people_entry": false,
"github_name": null,
"prs": 5,
"commits": 5,
"lines_added": 7086,
"lines_deleted": 493,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-05T05:01:36Z",
"last_commit": "2026-10-07T21:59:16Z"
},
{
"login": "Ni-zav",
"people_entry": false,
"github_name": null,
"prs": 12,
"commits": 12,
"lines_added": 488,
"lines_deleted": 42,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-05T05:01:49Z",
"last_commit": "2026-10-07T21:57:43Z"
},
{
"login": "kntrttm",
"people_entry": false,
"github_name": null,
"prs": 3,
"commits": 3,
"lines_added": 4228,
"lines_deleted": 1172,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-05T05:02:52Z",
"last_commit": "2026-10-05T23:40:10Z"
},
{
"login": "kevin9327",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 15,
"lines_deleted": 2,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-05T08:14:40Z",
"last_commit": "2026-10-05T08:14:40Z"
},
{
"login": "mekanix",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 29,
"lines_deleted": 8,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-05T13:01:58Z",
"last_commit": "2026-10-05T13:01:58Z"
},
{
"login": "nuIIpointerexception",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 82,
"lines_deleted": 9,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-05T13:02:14Z",
"last_commit": "2026-10-05T13:02:14Z"
},
{
"login": "john8675309",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 89,
"lines_deleted": 0,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-05T13:58:45Z",
"last_commit": "2026-10-05T13:58:45Z"
},
{
"login": "CQMHV",
"people_entry": false,
"github_name": null,
"prs": 9,
"commits": 9,
"lines_added": 2841,
"lines_deleted": 23,
"binary_added": 2,
"binary_deleted": 0,
"first_commit": "2026-10-05T22:13:51Z",
"last_commit": "2026-10-07T20:43:03Z"
},
{
"login": "kuishou68",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 6,
"lines_deleted": 1931,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-06T11:43:03Z",
"last_commit": "2026-10-06T11:43:03Z"
},
{
"login": "midasdf",
"people_entry": false,
"github_name": null,
"prs": 2,
"commits": 2,
"lines_added": 199,
"lines_deleted": 25,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-06T11:44:08Z",
"last_commit": "2026-10-07T22:53:56Z"
},
{
"login": "bommerts",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 30,
"lines_deleted": 3,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-06T14:46:41Z",
"last_commit": "2026-10-06T14:46:41Z"
},
{
"login": "leonaci",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 202,
"lines_deleted": 26,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-06T15:27:54Z",
"last_commit": "2026-10-06T15:27:54Z"
},
{
"login": "juansebsol",
"people_entry": false,
"github_name": null,
"prs": 2,
"commits": 2,
"lines_added": 800,
"lines_deleted": 118,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-06T15:28:09Z",
"last_commit": "2026-10-07T12:40:00Z"
},
{
"login": "WildfootW",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 2027,
"lines_deleted": 17,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-06T15:28:23Z",
"last_commit": "2026-10-06T15:28:23Z"
},
{
"login": "Mawfyy",
"people_entry": false,
"github_name": null,
"prs": 3,
"commits": 3,
"lines_added": 2213,
"lines_deleted": 4,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-06T15:28:37Z",
"last_commit": "2026-10-07T20:12:00Z"
},
{
"login": "petrleocompel",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 2025,
"lines_deleted": 2,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-06T15:29:44Z",
"last_commit": "2026-10-06T15:29:44Z"
},
{
"login": "bflatastic",
"people_entry": false,
"github_name": null,
"prs": 35,
"commits": 35,
"lines_added": 6009,
"lines_deleted": 1253,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T00:59:14Z",
"last_commit": "2026-10-08T01:16:38Z"
},
{
"login": "Pedroj-64",
"people_entry": false,
"github_name": null,
"prs": 2,
"commits": 2,
"lines_added": 141,
"lines_deleted": 9,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T03:28:01Z",
"last_commit": "2026-10-07T03:43:31Z"
},
{
"login": "floflo777",
"people_entry": false,
"github_name": null,
"prs": 4,
"commits": 4,
"lines_added": 163,
"lines_deleted": 31,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T03:34:03Z",
"last_commit": "2026-10-07T11:05:38Z"
},
{
"login": "HEOJUNFO",
"people_entry": false,
"github_name": null,
"prs": 2,
"commits": 2,
"lines_added": 55,
"lines_deleted": 0,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T03:34:17Z",
"last_commit": "2026-10-07T03:34:30Z"
},
{
"login": "Ar-maan05",
"people_entry": false,
"github_name": null,
"prs": 7,
"commits": 7,
"lines_added": 840,
"lines_deleted": 111,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T03:34:44Z",
"last_commit": "2026-10-07T20:30:52Z"
},
{
"login": "agorevski",
"people_entry": false,
"github_name": null,
"prs": 30,
"commits": 30,
"lines_added": 3353,
"lines_deleted": 404,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T03:34:59Z",
"last_commit": "2026-10-07T21:14:03Z"
},
{
"login": "naumanAhmed3",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 14,
"lines_deleted": 2,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T03:38:32Z",
"last_commit": "2026-10-07T03:38:32Z"
},
{
"login": "ljkx",
"people_entry": false,
"github_name": null,
"prs": 5,
"commits": 5,
"lines_added": 7478,
"lines_deleted": 178,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T03:43:18Z",
"last_commit": "2026-10-08T01:12:38Z"
},
{
"login": "carterwsmith",
"people_entry": false,
"github_name": null,
"prs": 4,
"commits": 4,
"lines_added": 1213,
"lines_deleted": 152,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T04:32:23Z",
"last_commit": "2026-10-07T22:57:50Z"
},
{
"login": "coygeek",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 4,
"lines_deleted": 0,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T04:32:34Z",
"last_commit": "2026-10-07T04:32:34Z"
},
{
"login": "neversaynv3r",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 89,
"lines_deleted": 0,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T04:40:40Z",
"last_commit": "2026-10-07T04:40:40Z"
},
{
"login": "ianswope",
"people_entry": false,
"github_name": null,
"prs": 2,
"commits": 2,
"lines_added": 82,
"lines_deleted": 9,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T09:26:01Z",
"last_commit": "2026-10-07T19:59:51Z"
},
{
"login": "TheSeanLavery",
"people_entry": false,
"github_name": null,
"prs": 4,
"commits": 4,
"lines_added": 2205,
"lines_deleted": 66,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T10:11:06Z",
"last_commit": "2026-10-07T11:45:26Z"
},
{
"login": "mnmlyw",
"people_entry": false,
"github_name": null,
"prs": 4,
"commits": 4,
"lines_added": 330,
"lines_deleted": 41,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T10:12:13Z",
"last_commit": "2026-10-08T01:20:47Z"
},
{
"login": "rosematcha",
"people_entry": false,
"github_name": null,
"prs": 12,
"commits": 12,
"lines_added": 1700,
"lines_deleted": 202,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T10:49:53Z",
"last_commit": "2026-10-08T00:18:27Z"
},
{
"login": "jason4short",
"people_entry": false,
"github_name": null,
"prs": 8,
"commits": 8,
"lines_added": 1653,
"lines_deleted": 101,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T10:58:01Z",
"last_commit": "2026-10-07T22:02:34Z"
},
{
"login": "siraxe",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 276,
"lines_deleted": 18,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T11:03:49Z",
"last_commit": "2026-10-07T11:03:49Z"
},
{
"login": "arpitkjaiswal",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 19,
"lines_deleted": 19,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T11:06:21Z",
"last_commit": "2026-10-07T11:06:21Z"
},
{
"login": "EthanSuquet",
"people_entry": false,
"github_name": null,
"prs": 5,
"commits": 5,
"lines_added": 405,
"lines_deleted": 56,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T11:40:10Z",
"last_commit": "2026-10-07T20:50:09Z"
},
{
"login": "alexanderalber",
"people_entry": false,
"github_name": null,
"prs": 3,
"commits": 3,
"lines_added": 1809,
"lines_deleted": 56,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T11:42:30Z",
"last_commit": "2026-10-07T22:59:39Z"
},
{
"login": "flyyangX",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 242,
"lines_deleted": 53,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T11:46:52Z",
"last_commit": "2026-10-07T11:46:52Z"
},
{
"login": "leblanc-simon",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 2527,
"lines_deleted": 4,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T11:49:30Z",
"last_commit": "2026-10-07T11:49:30Z"
},
{
"login": "trokute",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 2484,
"lines_deleted": 1,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T11:52:51Z",
"last_commit": "2026-10-07T11:52:51Z"
},
{
"login": "ha1des",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 1328,
"lines_deleted": 161,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T13:02:58Z",
"last_commit": "2026-10-07T13:02:58Z"
},
{
"login": "neko233-com",
"people_entry": false,
"github_name": null,
"prs": 2,
"commits": 2,
"lines_added": 2719,
"lines_deleted": 73,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T13:06:33Z",
"last_commit": "2026-10-07T20:52:55Z"
},
{
"login": "Sirsyorrz",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 61,
"lines_deleted": 7,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T13:11:35Z",
"last_commit": "2026-10-07T13:11:35Z"
},
{
"login": "Elvis33LE",
"people_entry": false,
"github_name": null,
"prs": 8,
"commits": 8,
"lines_added": 3929,
"lines_deleted": 91,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T13:11:45Z",
"last_commit": "2026-10-08T01:10:38Z"
},
{
"login": "cristianmoroaica",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 34,
"lines_deleted": 6,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T13:12:11Z",
"last_commit": "2026-10-07T13:12:11Z"
},
{
"login": "Webificio",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 232,
"lines_deleted": 2,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T13:12:44Z",
"last_commit": "2026-10-07T13:12:44Z"
},
{
"login": "mvanhorn",
"people_entry": false,
"github_name": null,
"prs": 4,
"commits": 4,
"lines_added": 2710,
"lines_deleted": 168,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T20:09:46Z",
"last_commit": "2026-10-07T21:12:52Z"
},
{
"login": "gangldiego",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 2592,
"lines_deleted": 10,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T20:11:38Z",
"last_commit": "2026-10-07T20:11:38Z"
},
{
"login": "kijoong-kim",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 1337,
"lines_deleted": 539,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T20:32:47Z",
"last_commit": "2026-10-07T20:32:47Z"
},
{
"login": "Cyanoxide",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 9,
"lines_deleted": 0,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T20:34:38Z",
"last_commit": "2026-10-07T20:34:38Z"
},
{
"login": "tanish2k09",
"people_entry": false,
"github_name": null,
"prs": 10,
"commits": 10,
"lines_added": 389,
"lines_deleted": 94,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T20:39:39Z",
"last_commit": "2026-10-07T23:57:32Z"
},
{
"login": "jerrykimo",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 442,
"lines_deleted": 6,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T20:57:21Z",
"last_commit": "2026-10-07T20:57:21Z"
},
{
"login": "bfenty",
"people_entry": false,
"github_name": null,
"prs": 10,
"commits": 10,
"lines_added": 490,
"lines_deleted": 21,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T21:45:54Z",
"last_commit": "2026-10-08T01:07:50Z"
},
{
"login": "Luna161",
"people_entry": false,
"github_name": null,
"prs": 4,
"commits": 4,
"lines_added": 89,
"lines_deleted": 15,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T22:00:59Z",
"last_commit": "2026-10-08T01:04:05Z"
},
{
"login": "abc2cb",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 1197,
"lines_deleted": 27,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T22:40:24Z",
"last_commit": "2026-10-07T22:40:24Z"
},
{
"login": "remidesbois1",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 1894,
"lines_deleted": 400,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T23:05:15Z",
"last_commit": "2026-10-07T23:05:15Z"
},
{
"login": "giallorosso84",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 2188,
"lines_deleted": 6,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T23:13:01Z",
"last_commit": "2026-10-07T23:13:01Z"
},
{
"login": "louwers",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 2528,
"lines_deleted": 21,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T23:42:45Z",
"last_commit": "2026-10-07T23:42:45Z"
},
{
"login": "simonwjackson",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 3,
"lines_deleted": 2,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-08T00:55:27Z",
"last_commit": "2026-10-08T00:55:27Z"
},
{
"login": "ajsongpilot",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 42,
"lines_deleted": 16,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-08T01:09:04Z",
"last_commit": "2026-10-08T01:09:04Z"
}
],
"models": [
{
"id": "anthropic/claude-opus/5.5",
"company": "Anthropic",
"model": "Claude Opus",
"version": "5.5",
"commits": 228,
"lines_added": 153184,
"lines_deleted": 22216
},
{
"id": "anthropic/claude-opus/4.8",
"company": "Anthropic",
"model": "Claude Opus",
"version": "4.8",
"commits": 45,
"lines_added": 3896,
"lines_deleted": 278
},
{
"id": "github/copilot",
"company": "GitHub",
"model": "Copilot",
"version": "",
"commits": 6,
"lines_added": 1094,
"lines_deleted": 61
},
{
"id": "anthropic/claude-mythos/5.1",
"company": "Anthropic",
"model": "Claude Mythos",
"version": "5.1",
"commits": 2,
"lines_added": 6195,
"lines_deleted": 100
},
{
"id": "anthropic/claude-sonnet/5.5",
"company": "Anthropic",
"model": "Claude Sonnet",
"version": "5.5",
"commits": 2,
"lines_added": 4161,
"lines_deleted": 1171
},
{
"id": "alibaba/qwen-flash/3.8",
"company": "Alibaba",
"model": "Qwen Flash",
"version": "3.8",
"commits": 1,
"lines_added": 25,
"lines_deleted": 5
},
{
"id": "anthropic/claude-haiku/4.5",
"company": "Anthropic",
"model": "Claude Haiku",
"version": "4.5",
"commits": 1,
"lines_added": 134,
"lines_deleted": 12
},
{
"id": "cognition/devin",
"company": "Cognition",
"model": "Devin",
"version": "",
"commits": 1,
"lines_added": 44,
"lines_deleted": 1
}
]
}

23
contributors/people.toml Normal file
View File

@ -0,0 +1,23 @@
# Contributor names for the About window (craftrules standards/contributors.md).
#
# Everyone who contributes is credited by GitHub username automatically. Anything more is up to
# you: add YOUR OWN entry below, in your own PR (commit it yourself). Never add someone else; a
# maintainer may add a person only when that person asked, recording where in consent_source.
#
# Easiest: python3 ../../craftrules/scripts/contributors.py --add-me . \
# [--real-name "Ada Lovelace"] [--display-name "Ada"] [--sync-github-name]
#
# Or copy this block, replacing your-github-username (every field is optional):
#
# [people.your-github-username]
# real_name = "Your Name" # shown when the viewer picks "Real name"
# display_name = "Nickname" # shown when the viewer picks "Display name"
# sync_github_name = true # or: use the name on your public GitHub profile as display name
#
# To remove your names, delete your entry (or set fields empty) in a PR. The next
# contributors.py run, and the next build, drop them.
[people.echelon]
real_name = "Brandon Thomas"
display_name = "Brandon Thomas"
sync_github_name = false

View File

@ -45,3 +45,7 @@ photocraft-codecs = { workspace = true }
photocraft-testkit = { workspace = true }
# Only for examples/perf_scenarios.rs (a 500-brush .abr for the #210 import row).
photocraft-psd = { workspace = true }
[build-dependencies]
serde_json = { workspace = true }
toml = "1"

102
crates/ui-egui/build.rs Normal file
View File

@ -0,0 +1,102 @@
//! Bake the contributor credits into the binary as static Rust tables (craftrules
//! standards/contributors.md). Nothing is read at run time.
//!
//! - contributors/contributors.json: commit stats per GitHub username, generated by craftrules
//! scripts/contributors.py, which also marks whose people.toml entry it verified.
//! - contributors/people.toml: the names people chose for themselves. A name is used only when the
//! JSON says the entry was verified (`people_entry`).
//!
//! A missing or malformed file gives empty tables (or no names) and a warning.
use std::fmt::Write as _;
use serde_json::Value;
fn main() {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../contributors");
let json = read(&dir.join("contributors.json"), "the About window shows no contributors", |t| serde_json::from_str::<Value>(t).map_err(|e| e.to_string()))
.unwrap_or(Value::Null);
let people =
read(&dir.join("people.toml"), "contributors are shown by username only", |t| t.parse::<toml::Table>().map_err(|e| e.to_string())).unwrap_or_default();
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap_or_default()).join("credits.rs");
if let Err(e) = std::fs::write(&out, generate(&json, &people)) {
println!("cargo::warning=writing {}: {e}", out.display());
}
}
fn read<T>(path: &std::path::Path, fallback: &str, parse: impl Fn(&str) -> Result<T, String>) -> Option<T> {
println!("cargo::rerun-if-changed={}", path.display());
let r = std::fs::read_to_string(path).map_err(|e| e.to_string()).and_then(|t| parse(&t));
if let Err(e) = &r {
println!("cargo::warning={}: {e}; {fallback}", path.display());
}
r.ok()
}
/// A non-empty string field of someone's people.toml entry, as a Rust `Option<&str>` literal.
fn name(v: Option<&str>) -> String {
match v.map(str::trim).filter(|s| !s.is_empty()) {
Some(x) => format!("Some({x:?})"),
None => "None".into(),
}
}
fn s(v: &Value, k: &str) -> String {
format!("{:?}", v.get(k).and_then(Value::as_str).unwrap_or(""))
}
fn n(v: &Value, k: &str) -> u64 {
v.get(k).and_then(Value::as_u64).unwrap_or(0)
}
fn rows(json: &Value, k: &str) -> Vec<Value> {
json.get(k).and_then(Value::as_array).cloned().unwrap_or_default()
}
fn generate(json: &Value, people: &toml::Table) -> String {
let mut o = String::new();
let _ = writeln!(o, "pub static TOTAL_COMMITS: u64 = {};", n(json, "commits"));
let _ = writeln!(o, "pub static CONTRIBUTORS: &[Contributor] = &[");
for c in rows(json, "contributors") {
let login = c.get("login").and_then(Value::as_str).unwrap_or("");
if login.is_empty() {
continue;
}
// Names only from a verified, self-chosen people.toml entry.
let entry = people.get("people").and_then(|p| p.get(login)).filter(|_| c.get("people_entry").and_then(Value::as_bool) == Some(true));
let field = |k: &str| entry.and_then(|e| e.get(k)).and_then(toml::Value::as_str);
let synced = entry.and_then(|e| e.get("sync_github_name")).and_then(toml::Value::as_bool) == Some(true);
let display =
field("display_name").filter(|s| !s.trim().is_empty()).or_else(|| if synced { c.get("github_name").and_then(Value::as_str) } else { None });
let _ = writeln!(
o,
" Contributor {{ login: {login:?}, display_name: {}, real_name: {}, prs: {}, commits: {}, lines_added: {}, lines_deleted: {}, binary_added: {}, binary_deleted: {}, first_commit: {}, last_commit: {} }},",
name(display),
name(field("real_name")),
n(&c, "prs"),
n(&c, "commits"),
n(&c, "lines_added"),
n(&c, "lines_deleted"),
n(&c, "binary_added"),
n(&c, "binary_deleted"),
s(&c, "first_commit"),
s(&c, "last_commit"),
);
}
let _ = writeln!(o, "];");
let _ = writeln!(o, "pub static MODELS: &[Model] = &[");
for m in rows(json, "models") {
let _ = writeln!(
o,
" Model {{ company: {}, model: {}, version: {}, commits: {}, lines_added: {}, lines_deleted: {} }},",
s(&m, "company"),
s(&m, "model"),
s(&m, "version"),
n(&m, "commits"),
n(&m, "lines_added"),
n(&m, "lines_deleted"),
);
}
let _ = writeln!(o, "];");
o
}

View File

@ -0,0 +1,398 @@
//! The About window's credits: every contributor (GitHub login always; display and real names only
//! with recorded consent) and the AI models that helped. The tables are baked into the binary by
//! build.rs from contributors/contributors.json (craftrules standards/contributors.md); nothing is
//! read at run time. UI strings go through `tl!` (column abbreviations like `+LOC` stay as they are).
use std::cmp::Ordering;
use egui::RichText;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Contributor {
pub login: &'static str,
pub display_name: Option<&'static str>,
pub real_name: Option<&'static str>,
pub prs: u64,
pub commits: u64,
pub lines_added: u64,
pub lines_deleted: u64,
pub binary_added: u64,
pub binary_deleted: u64,
/// ISO 8601 UTC; empty for someone credited only through a merged PR.
pub first_commit: &'static str,
pub last_commit: &'static str,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Model {
pub company: &'static str,
pub model: &'static str,
pub version: &'static str,
pub commits: u64,
pub lines_added: u64,
pub lines_deleted: u64,
}
include!(concat!(env!("OUT_DIR"), "/credits.rs"));
/// Which name to show. Display and real names fall back to the `@username` when not given.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum NameMode {
#[default]
Username,
DisplayName,
RealName,
}
impl NameMode {
pub const ALL: [NameMode; 3] = [NameMode::Username, NameMode::DisplayName, NameMode::RealName];
pub fn label(self) -> &'static str {
match self {
NameMode::Username => tl!("Username"),
NameMode::DisplayName => tl!("Display name"),
NameMode::RealName => tl!("Real name"),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SortKey {
Name,
Prs,
Commits,
LinesAdded,
LinesDeleted,
LinesDelta,
BinaryAdded,
BinaryDeleted,
#[default]
FirstCommit,
LastCommit,
}
impl SortKey {
pub const ALL: [SortKey; 10] = [
SortKey::Name,
SortKey::Prs,
SortKey::Commits,
SortKey::LinesAdded,
SortKey::LinesDeleted,
SortKey::LinesDelta,
SortKey::BinaryAdded,
SortKey::BinaryDeleted,
SortKey::FirstCommit,
SortKey::LastCommit,
];
/// Menu label and table header, in the UI language.
pub fn label(self) -> (&'static str, &'static str) {
match self {
SortKey::Name => (tl!("Name (A–Z)"), tl!("Name")),
SortKey::Prs => (tl!("Merged PRs"), "PRs"),
SortKey::Commits => (tl!("Commits"), tl!("Commits")),
SortKey::LinesAdded => (tl!("Lines added"), "+LOC"),
SortKey::LinesDeleted => (tl!("Lines deleted"), "−LOC"),
SortKey::LinesDelta => (tl!("Line delta"), "ΔLOC"),
SortKey::BinaryAdded => (tl!("Binary assets added"), "+Bin"),
SortKey::BinaryDeleted => (tl!("Binary assets removed"), "−Bin"),
SortKey::FirstCommit => (tl!("First commit"), tl!("First")),
SortKey::LastCommit => (tl!("Last commit"), tl!("Last")),
}
}
/// Names and dates read naturally oldest/A first; counts read biggest first.
pub fn default_ascending(self) -> bool {
matches!(self, SortKey::Name | SortKey::FirstCommit | SortKey::LastCommit)
}
}
impl Contributor {
pub fn name(&self, mode: NameMode) -> String {
let chosen = match mode {
NameMode::Username => None,
NameMode::DisplayName => self.display_name,
NameMode::RealName => self.real_name,
};
chosen.map_or_else(|| format!("@{}", self.login), str::to_string)
}
pub fn lines_delta(&self) -> i128 {
i128::from(self.lines_added) - i128::from(self.lines_deleted)
}
/// One line with everything we know, for tooltips.
pub fn summary(&self) -> String {
crate::i18n::fmt(
tl!("@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last}"),
&[
("login", self.login),
("prs", &group(self.prs)),
("commits", &group(self.commits)),
("added", &group(self.lines_added)),
("deleted", &group(self.lines_deleted)),
("delta", &signed(self.lines_delta())),
("binAdded", &group(self.binary_added)),
("binDeleted", &group(self.binary_deleted)),
("first", day(self.first_commit)),
("last", day(self.last_commit)),
],
)
}
}
/// Alphabetical key: case-insensitive, ignoring the `@` of usernames.
fn name_key(name: &str) -> String {
name.trim_start_matches('@').to_lowercase()
}
fn compare(a: &Contributor, b: &Contributor, mode: NameMode, key: SortKey) -> Ordering {
match key {
SortKey::Name => name_key(&a.name(mode)).cmp(&name_key(&b.name(mode))),
SortKey::Prs => a.prs.cmp(&b.prs),
SortKey::Commits => a.commits.cmp(&b.commits),
SortKey::LinesAdded => a.lines_added.cmp(&b.lines_added),
SortKey::LinesDeleted => a.lines_deleted.cmp(&b.lines_deleted),
SortKey::LinesDelta => a.lines_delta().cmp(&b.lines_delta()),
SortKey::BinaryAdded => a.binary_added.cmp(&b.binary_added),
SortKey::BinaryDeleted => a.binary_deleted.cmp(&b.binary_deleted),
// A PR-only contributor (no commit date) sorts after everyone with a date.
SortKey::FirstCommit => dated(a.first_commit).cmp(&dated(b.first_commit)),
SortKey::LastCommit => dated(a.last_commit).cmp(&dated(b.last_commit)),
}
}
fn dated(d: &str) -> (bool, &str) {
(d.is_empty(), d)
}
/// Contributors in display order. Ties fall back to the name (A–Z) so the order is stable.
pub fn sorted(list: &[Contributor], mode: NameMode, key: SortKey, ascending: bool) -> Vec<&Contributor> {
let mut v: Vec<&Contributor> = list.iter().collect();
v.sort_by(|a, b| {
let o = compare(a, b, mode, key);
let o = if ascending { o } else { o.reverse() };
o.then_with(|| compare(a, b, mode, SortKey::Name)).then_with(|| a.login.cmp(b.login))
});
v
}
fn group(n: u64) -> String {
let s = n.to_string();
let mut out = String::with_capacity(s.len() + s.len() / 3);
for (i, ch) in s.chars().enumerate() {
if i > 0 && (s.len() - i).is_multiple_of(3) {
out.push(',');
}
out.push(ch);
}
out
}
fn signed(n: i128) -> String {
let g = group(u64::try_from(n.unsigned_abs()).unwrap_or(u64::MAX));
if n < 0 { format!("−{g}") } else { format!("+{g}") }
}
fn day(iso: &str) -> &str {
if iso.is_empty() { "—" } else { iso.get(..10).unwrap_or(iso) }
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct View {
names: NameMode,
key: SortKey,
ascending: bool,
table: bool,
}
impl Default for View {
fn default() -> Self {
View { names: NameMode::Username, key: SortKey::FirstCommit, ascending: true, table: false }
}
}
/// About ▸ Contributors: a name toggle, a sort, and the list as a grab bag or a table.
pub fn contributors_ui(ui: &mut egui::Ui) {
let id = egui::Id::new("credits_view");
let mut v = ui.data_mut(|d| d.get_temp::<View>(id)).unwrap_or_default();
ui.horizontal_wrapped(|ui| {
ui.label(tl!("Show"));
for m in NameMode::ALL {
if ui.selectable_label(v.names == m, m.label()).clicked() {
v.names = m;
}
}
ui.separator();
ui.label(tl!("Sort"));
egui::ComboBox::from_id_salt("credits_sort").selected_text(v.key.label().0).show_ui(ui, |ui| {
for k in SortKey::ALL {
if ui.selectable_label(v.key == k, k.label().0).clicked() {
v.key = k;
v.ascending = k.default_ascending();
}
}
});
if ui.button(if v.ascending { "▲" } else { "▼" }).on_hover_text(tl!("Reverse the order")).clicked() {
v.ascending = !v.ascending;
}
ui.separator();
if ui.selectable_label(!v.table, tl!("Grab bag")).clicked() {
v.table = false;
}
if ui.selectable_label(v.table, tl!("Table")).clicked() {
v.table = true;
}
});
let list = sorted(CONTRIBUTORS, v.names, v.key, v.ascending);
let counts = crate::i18n::fmt(
tl!("Contributors: {contributors} · Commits: {commits}"),
&[("contributors", &group(list.len() as u64)), ("commits", &group(TOTAL_COMMITS))],
);
ui.label(RichText::new(counts).small().weak());
ui.separator();
egui::ScrollArea::both().auto_shrink([false, false]).max_height(340.0).show(ui, |ui| {
if list.is_empty() {
ui.label(tl!("No contributor data was built into this copy."));
} else if v.table {
table(ui, &list, &mut v);
} else {
ui.horizontal_wrapped(|ui| {
for (i, c) in list.iter().enumerate() {
if i > 0 {
ui.label(RichText::new("·").weak());
}
ui.hyperlink_to(c.name(v.names), format!("https://github.com/{}", c.login)).on_hover_text(c.summary());
}
});
}
});
ui.data_mut(|d| d.insert_temp(id, v));
}
fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View) {
egui::Grid::new("credits_table").striped(true).num_columns(SortKey::ALL.len()).show(ui, |ui| {
for k in SortKey::ALL {
let arrow = if v.key == k { if v.ascending { " ▲" } else { " ▼" } } else { "" };
if ui.button(RichText::new(format!("{}{arrow}", k.label().1)).strong()).on_hover_text(k.label().0).clicked() {
if v.key == k {
v.ascending = !v.ascending;
} else {
v.key = k;
v.ascending = k.default_ascending();
}
}
}
ui.end_row();
for c in list {
ui.hyperlink_to(c.name(v.names), format!("https://github.com/{}", c.login)).on_hover_text(c.summary());
ui.label(group(c.prs));
ui.label(group(c.commits));
ui.label(group(c.lines_added));
ui.label(group(c.lines_deleted));
ui.label(signed(c.lines_delta()));
ui.label(group(c.binary_added));
ui.label(group(c.binary_deleted));
ui.label(day(c.first_commit));
ui.label(day(c.last_commit));
ui.end_row();
}
});
}
/// About ▸ Models: AI models credited in Co-Authored-By trailers.
pub fn models_ui(ui: &mut egui::Ui) {
if MODELS.is_empty() {
ui.label(tl!("No model credits were built into this copy."));
return;
}
let assisted: u64 = MODELS.iter().map(|m| m.commits).max().unwrap_or(0).max(1);
egui::ScrollArea::both().auto_shrink([false, false]).max_height(340.0).show(ui, |ui| {
egui::Grid::new("credits_models").striped(true).num_columns(6).show(ui, |ui| {
for h in [tl!("Company"), tl!("Model"), tl!("Version"), tl!("Commits"), tl!("% of all commits"), tl!("Lines +/−")] {
ui.label(RichText::new(h).strong());
}
ui.end_row();
for m in MODELS {
ui.label(m.company);
ui.label(m.model);
ui.label(m.version);
ui.label(group(m.commits));
ui.label(format!("{:.0}%", 100.0 * m.commits as f64 / TOTAL_COMMITS.max(assisted) as f64));
ui.label(format!("+{} / −{}", group(m.lines_added), group(m.lines_deleted)));
ui.end_row();
}
});
});
}
#[cfg(test)]
mod tests {
use super::*;
fn c(login: &'static str, display: Option<&'static str>, real: Option<&'static str>, added: u64, deleted: u64, first: &'static str) -> Contributor {
Contributor {
login,
display_name: display,
real_name: real,
prs: 0,
commits: 1,
lines_added: added,
lines_deleted: deleted,
binary_added: 0,
binary_deleted: 0,
first_commit: first,
last_commit: first,
}
}
fn people() -> [Contributor; 4] {
[
c("zed", None, None, 5, 50, "2026-01-02T00:00:00Z"),
c("Bob", Some("Ann"), None, 10, 0, "2026-01-01T00:00:00Z"),
c("alice", None, None, 7, 1, ""),
c("carol", None, Some("Dana"), 1, 0, "2026-01-03T00:00:00Z"),
]
}
fn logins(v: &[&Contributor]) -> Vec<&'static str> {
v.iter().map(|c| c.login).collect()
}
#[test]
fn names_fall_back_to_the_username() {
let p = c("bob", Some("Bobby"), None, 0, 0, "");
assert_eq!(p.name(NameMode::Username), "@bob");
assert_eq!(p.name(NameMode::DisplayName), "Bobby");
assert_eq!(p.name(NameMode::RealName), "@bob");
}
#[test]
fn alphabetical_ignores_at_and_case() {
let all = people();
let v = sorted(&all, NameMode::Username, SortKey::Name, true);
assert_eq!(logins(&v), ["alice", "Bob", "carol", "zed"]);
let v = sorted(&all, NameMode::DisplayName, SortKey::Name, true);
assert_eq!(logins(&v), ["alice", "Bob", "carol", "zed"]); // "Ann" sorts with the @usernames
let v = sorted(&all, NameMode::RealName, SortKey::Name, false);
assert_eq!(logins(&v), ["zed", "carol", "Bob", "alice"]); // "Dana" between bob and zed
}
#[test]
fn delta_and_dates() {
let all = people();
let v = sorted(&all, NameMode::Username, SortKey::LinesDelta, false);
assert_eq!(logins(&v), ["Bob", "alice", "carol", "zed"]);
let v = sorted(&all, NameMode::Username, SortKey::FirstCommit, true);
assert_eq!(logins(&v), ["Bob", "zed", "carol", "alice"]); // undated last
}
#[test]
fn formatting() {
assert_eq!(group(1234567), "1,234,567");
assert_eq!(group(12), "12");
assert_eq!(signed(-1000), "−1,000");
assert_eq!(day("2026-10-07T18:45:23Z"), "2026-10-07");
assert_eq!(day(""), "—");
}
}

View File

@ -99,8 +99,15 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
if d.kind == DialogKind::NewDocument {
ui.set_min_width(800.0);
}
// The About window: room for the contributor table, the same width on every tab.
let about_tabs = d.kind == DialogKind::About && d.fields.get("systemInfo").and_then(Value::as_bool) != Some(true);
if about_tabs {
ui.set_min_width(700.0);
}
ui.set_max_width(wide.unwrap_or(if d.kind == DialogKind::NewDocument {
800.0
} else if about_tabs {
700.0
} else if d.kind == DialogKind::LayerStyle || d.fields.contains_key("__export") || crate::color_picker_ui::owns(&d.fields) {
600.0
} else {
@ -129,16 +136,37 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
}
}
DialogKind::About => {
ui.label(tl!("PhotoCraft — an open-source, native image editor written in Rust."));
ui.label(crate::i18n::fmt(tl!("Version {version}"), &[("version", &photocraft_engine::build_info::long_version())]));
ui.add_space(12.0);
ui.vertical_centered(|ui| {
crate::links::discord_button(app, ui, 220.0);
ui.add_space(8.0);
crate::links::link_row(app, ui);
// Tabs About · Contributors · Models (craftrules standards/contributors.md). The
// tab is a dialog field, so automation can switch it with `ui.dialog.set`.
let tab = about_tab(&fields);
let mut chosen = tab;
ui.horizontal(|ui| {
for (key, label) in [("about", tl!("About")), ("contributors", tl!("Contributors")), ("models", tl!("Models"))] {
if crate::widgets::pill_tab(ui, label, tab == key).clicked() {
chosen = key;
}
}
});
ui.add_space(10.0);
ui.weak("egui · wgpu · photocraft-engine");
if chosen != tab {
fields.insert("tab".into(), json!(chosen));
}
ui.add_space(8.0);
match chosen {
"contributors" => crate::credits::contributors_ui(ui),
"models" => crate::credits::models_ui(ui),
_ => {
ui.label(tl!("PhotoCraft — an open-source, native image editor written in Rust."));
ui.label(crate::i18n::fmt(tl!("Version {version}"), &[("version", &photocraft_engine::build_info::long_version())]));
ui.add_space(12.0);
ui.vertical_centered(|ui| {
crate::links::discord_button(app, ui, 220.0);
ui.add_space(8.0);
crate::links::link_row(app, ui);
});
ui.add_space(10.0);
ui.weak("egui · wgpu · photocraft-engine");
}
}
}
DialogKind::Command if crate::fill_ui::owns(&fields) => crate::fill_ui::body(app, ui, &mut fields),
DialogKind::Command if crate::rasterize_prompt::owns(&fields) => crate::rasterize_prompt::body(ui, &fields),
@ -239,6 +267,15 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
ctx.data_mut(|m| m.insert_temp(egui::Id::new(RECTS), shown));
}
/// The About window's tabs, as stored in its `tab` field.
pub const ABOUT_TABS: [&str; 3] = ["about", "contributors", "models"];
/// The About tab to show: the `tab` field when it names one, otherwise "about".
fn about_tab(fields: &serde_json::Map<String, Value>) -> &'static str {
let want = fields.get("tab").and_then(Value::as_str).unwrap_or("");
ABOUT_TABS.iter().copied().find(|t| *t == want).unwrap_or("about")
}
/// The dialog title as shown: [`title`] in the UI language.
fn display_title(d: &Dialog) -> String {
match d.kind {

View File

@ -2093,3 +2093,36 @@ cameraRaw Color Barvy
Magnetic Lasso Tool Nástroj magnetické laso
Frequency Frekvence
Use tablet pressure to change pen width Měnit šířku pera přítlakem tabletu
# About ▸ Contributors / Models (credits, craftrules standards/contributors.md)
About O aplikaci
Contributors Přispěvatelé
Models Modely
Sort Řadit
Username Uživatelské jméno
Display name Zobrazované jméno
Real name Skutečné jméno
Reverse the order Obrátit pořadí
Grab bag Výčet
Table Tabulka
Name (A–Z) Jméno (A–Z)
Merged PRs Sloučené PR
Commits Commity
Lines added Přidané řádky
Lines deleted Smazané řádky
Line delta Rozdíl řádků
Binary assets added Přidané binární soubory
Binary assets removed Odebrané binární soubory
First commit První commit
First První
Last commit Poslední commit
Last Poslední
Contributors: {contributors} · Commits: {commits} Přispěvatelé: {contributors} · Commity: {commits}
No contributor data was built into this copy. Tato kopie neobsahuje údaje o přispěvatelích.
No model credits were built into this copy. Tato kopie neobsahuje údaje o modelech.
Company Společnost
Model Model
Version Verze
% of all commits % všech commitů
Lines +/− Řádky +/−
@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last} @{login}: PR: {prs}, commity: {commits}, řádky +{added} / −{deleted} (Δ {delta}), binární soubory +{binAdded} / −{binDeleted}, {first} – {last}

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2135,3 +2135,34 @@
Magnetic Lasso Tool Magnetisches-Lasso-Werkzeug
Frequency Frequenz
Use tablet pressure to change pen width Tablettdruck zum Ändern der Stiftbreite verwenden
# About ▸ Contributors / Models (credits, craftrules standards/contributors.md)
About Über
Contributors Mitwirkende
Models Modelle
Sort Sortieren
Username Benutzername
Display name Anzeigename
Real name Echter Name
Reverse the order Reihenfolge umkehren
Grab bag Wortwolke
Table Tabelle
Name (A–Z) Name (A–Z)
Merged PRs Gemergte PRs
Commits Commits
Lines added Hinzugefügte Zeilen
Lines deleted Gelöschte Zeilen
Line delta Zeilensaldo
Binary assets added Hinzugefügte Binärdateien
Binary assets removed Entfernte Binärdateien
First commit Erster Commit
First Erster
Last commit Letzter Commit
Last Letzter
Contributors: {contributors} · Commits: {commits} Mitwirkende: {contributors} · Commits: {commits}
No contributor data was built into this copy. In diese Kopie wurden keine Mitwirkendendaten eingebaut.
No model credits were built into this copy. In diese Kopie wurden keine Modellnennungen eingebaut.
Company Firma
Version Version
% of all commits % aller Commits
Lines +/− Zeilen +/−
@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last} @{login}: {prs} PRs, {commits} Commits, +{added} / −{deleted} Zeilen (Δ {delta}), +{binAdded} / −{binDeleted} Binärdateien, {first} – {last}

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2166,3 +2166,35 @@ cameraRaw Amount Cantidad
Magnetic Lasso Tool Herramienta Lazo magnético
Frequency Frecuencia
Use tablet pressure to change pen width Usar la presión de la tableta para cambiar la anchura del lápiz
# About ▸ Contributors / Models (credits, craftrules standards/contributors.md)
About Acerca de
Contributors Colaboradores
Models Modelos
Sort Ordenar
Username Nombre de usuario
Display name Nombre visible
Real name Nombre real
Reverse the order Invertir el orden
Grab bag Lista libre
Table Tabla
Name (A–Z) Nombre (A–Z)
Merged PRs PR fusionadas
Commits Commits
Lines added Líneas añadidas
Lines deleted Líneas eliminadas
Line delta Balance de líneas
Binary assets added Archivos binarios añadidos
Binary assets removed Archivos binarios eliminados
First commit Primer commit
First Primero
Last commit Último commit
Last Último
Contributors: {contributors} · Commits: {commits} Colaboradores: {contributors} · Commits: {commits}
No contributor data was built into this copy. Esta copia no incluye datos de colaboradores.
No model credits were built into this copy. Esta copia no incluye créditos de modelos.
Company Empresa
Version Versión
% of all commits % de todos los commits
Lines +/− Líneas +/−
@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last} @{login}: {prs} PR, {commits} commits, +{added} / −{deleted} líneas (Δ {delta}), +{binAdded} / −{binDeleted} archivos binarios, {first} – {last}

Can't render this file because it contains an unexpected character in line 4 and column 47.

View File

@ -2526,3 +2526,35 @@ cameraRaw Blending Fusion
Magnetic Lasso Tool Outil Lasso magnétique
Frequency Fréquence
Use tablet pressure to change pen width Utiliser la pression de la tablette pour modifier la largeur de la plume
# About ▸ Contributors / Models (credits, craftrules standards/contributors.md)
About À propos
Contributors Contributeurs
Models Modèles
Sort Trier
Username Nom d'utilisateur
Display name Nom affiché
Real name Nom réel
Reverse the order Inverser l'ordre
Grab bag Vrac
Table Tableau
Name (A–Z) Nom (A–Z)
Merged PRs PR fusionnées
Commits Commits
Lines added Lignes ajoutées
Lines deleted Lignes supprimées
Line delta Solde de lignes
Binary assets added Fichiers binaires ajoutés
Binary assets removed Fichiers binaires supprimés
First commit Premier commit
First Premier
Last commit Dernier commit
Last Dernier
Contributors: {contributors} · Commits: {commits} Contributeurs : {contributors} · Commits : {commits}
No contributor data was built into this copy. Aucune donnée de contributeurs n'est incluse dans cette copie.
No model credits were built into this copy. Aucun crédit de modèle n'est inclus dans cette copie.
Company Société
Version Version
% of all commits % de tous les commits
Lines +/− Lignes +/−
@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last} @{login} : {prs} PR, {commits} commits, +{added} / −{deleted} lignes (Δ {delta}), +{binAdded} / −{binDeleted} fichiers binaires, {first} – {last}

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2513,3 +2513,35 @@ cameraRaw Blending Pembauran
Magnetic Lasso Tool Alat Magnetic Lasso
Frequency Frekuensi
Use tablet pressure to change pen width Gunakan tekanan tablet untuk mengubah lebar pena
# About ▸ Contributors / Models (credits, craftrules standards/contributors.md)
About Tentang
Contributors Kontributor
Models Model
Sort Urutkan
Username Nama pengguna
Display name Nama tampilan
Real name Nama asli
Reverse the order Balik urutan
Grab bag Daftar bebas
Table Tabel
Name (A–Z) Nama (A–Z)
Merged PRs PR yang digabung
Commits Commit
Lines added Baris ditambah
Lines deleted Baris dihapus
Line delta Selisih baris
Binary assets added Berkas biner ditambah
Binary assets removed Berkas biner dihapus
First commit Commit pertama
First Pertama
Last commit Commit terakhir
Last Terakhir
Contributors: {contributors} · Commits: {commits} Kontributor: {contributors} · Commit: {commits}
No contributor data was built into this copy. Salinan ini tidak memuat data kontributor.
No model credits were built into this copy. Salinan ini tidak memuat kredit model.
Company Perusahaan
Version Versi
% of all commits % dari semua commit
Lines +/− Baris +/−
@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last} @{login}: {prs} PR, {commits} commit, +{added} / −{deleted} baris (Δ {delta}), +{binAdded} / −{binDeleted} berkas biner, {first} – {last}

Can't render this file because it contains an unexpected character in line 4 and column 47.

View File

@ -2168,3 +2168,35 @@ cameraRaw Amount Quantità
Magnetic Lasso Tool Strumento Lazo magnetico
Frequency Frequenza
Use tablet pressure to change pen width Usa pressione tavoletta per cambiare la larghezza della penna
# About ▸ Contributors / Models (credits, craftrules standards/contributors.md)
About Informazioni
Contributors Collaboratori
Models Modelli
Sort Ordina
Username Nome utente
Display name Nome visualizzato
Real name Nome reale
Reverse the order Inverti l'ordine
Grab bag Elenco libero
Table Tabella
Name (A–Z) Nome (A–Z)
Merged PRs PR unite
Commits Commit
Lines added Righe aggiunte
Lines deleted Righe eliminate
Line delta Differenza righe
Binary assets added Risorse binarie aggiunte
Binary assets removed Risorse binarie rimosse
First commit Primo commit
First Primo
Last commit Ultimo commit
Last Ultimo
Contributors: {contributors} · Commits: {commits} Collaboratori: {contributors} · Commit: {commits}
No contributor data was built into this copy. Questa copia non include dati sui collaboratori.
No model credits were built into this copy. Questa copia non include i crediti dei modelli.
Company Azienda
Version Versione
% of all commits % di tutti i commit
Lines +/− Righe +/−
@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last} @{login}: {prs} PR, {commits} commit, +{added} / −{deleted} righe (Δ {delta}), +{binAdded} / −{binDeleted} risorse binarie, {first} – {last}

Can't render this file because it has a wrong number of fields in line 14.

View File

@ -2077,3 +2077,36 @@ cameraRaw Color カラー
Magnetic Lasso Tool マグネット選択ツール
Frequency 頻度
Use tablet pressure to change pen width タブレットの筆圧を使用してペンの幅を変更
# About ▸ Contributors / Models (credits, craftrules standards/contributors.md)
About バージョン情報
Contributors コントリビューター
Models モデル
Sort 並べ替え
Username ユーザー名
Display name 表示名
Real name 本名
Reverse the order 順序を反転
Grab bag 一覧
Table 表
Name (A–Z) 名前 (A–Z)
Merged PRs マージされた PR
Commits コミット
Lines added 追加行数
Lines deleted 削除行数
Line delta 行数の増減
Binary assets added 追加したバイナリ
Binary assets removed 削除したバイナリ
First commit 最初のコミット
First 最初
Last commit 最後のコミット
Last 最後
Contributors: {contributors} · Commits: {commits} コントリビューター: {contributors} · コミット: {commits}
No contributor data was built into this copy. このコピーにはコントリビューターのデータが含まれていません。
No model credits were built into this copy. このコピーにはモデルのクレジットが含まれていません。
Company 会社
Model モデル
Version バージョン
% of all commits 全コミットに占める割合 (%)
Lines +/− 行数 +/−
@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last} @{login}: PR {prs} 件、コミット {commits} 件、行 +{added} / −{deleted} (Δ {delta})、バイナリ +{binAdded} / −{binDeleted}、{first} – {last}

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2679,3 +2679,35 @@ cameraRaw Color 색상
Magnetic Lasso Tool 자석 올가미 도구
Frequency 빈도
Use tablet pressure to change pen width 태블릿 압력을 사용하여 펜 폭 변경
# About ▸ Contributors / Models (credits, craftrules standards/contributors.md)
About 정보
Contributors 기여자
Models 모델
Sort 정렬
Username 사용자 이름
Display name 표시 이름
Real name 실명
Reverse the order 순서 뒤집기
Grab bag 나열
Table 표
Name (A–Z) 이름 (A–Z)
Merged PRs 병합된 PR
Commits 커밋
Lines added 추가된 줄
Lines deleted 삭제된 줄
Line delta 줄 증감
Binary assets added 추가된 바이너리
Binary assets removed 삭제된 바이너리
First commit 첫 커밋
First 처음
Last commit 마지막 커밋
Last 마지막
Contributors: {contributors} · Commits: {commits} 기여자: {contributors} · 커밋: {commits}
No contributor data was built into this copy. 이 사본에는 기여자 데이터가 포함되어 있지 않습니다.
No model credits were built into this copy. 이 사본에는 모델 크레딧이 포함되어 있지 않습니다.
Company 회사
Version 버전
% of all commits 전체 커밋 대비 %
Lines +/− 줄 +/−
@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last} @{login}: PR {prs}개, 커밋 {commits}개, 줄 +{added} / −{deleted} (Δ {delta}), 바이너리 +{binAdded} / −{binDeleted}, {first} – {last}

Can't render this file because it has a wrong number of fields in line 10.

View File

@ -2534,3 +2534,34 @@ cameraRaw Whites Brancos
Magnetic Lasso Tool Ferramenta Laço magnético
Frequency Frequência
Use tablet pressure to change pen width Usar a pressão da mesa digitalizadora para alterar a largura da caneta
# About ▸ Contributors / Models (credits, craftrules standards/contributors.md)
About Sobre
Contributors Colaboradores
Models Modelos
Sort Ordenar
Username Nome de usuário
Display name Nome de exibição
Real name Nome real
Reverse the order Inverter a ordem
Grab bag Lista corrida
Table Tabela
Name (A–Z) Nome (A–Z)
Merged PRs PRs mesclados
Commits Commits
Lines added Linhas adicionadas
Lines deleted Linhas removidas
Line delta Saldo de linhas
Binary assets added Arquivos binários adicionados
Binary assets removed Arquivos binários removidos
First commit Primeiro commit
First Primeiro
Last commit Último commit
Last Último
Contributors: {contributors} · Commits: {commits} Colaboradores: {contributors} · Commits: {commits}
No contributor data was built into this copy. Esta cópia não inclui dados de colaboradores.
No model credits were built into this copy. Esta cópia não inclui créditos de modelos.
Company Empresa
Version Versão
% of all commits % de todos os commits
Lines +/− Linhas +/−
@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last} @{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} linhas (Δ {delta}), +{binAdded} / −{binDeleted} arquivos binários, {first} – {last}

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2369,3 +2369,35 @@ cameraRaw Roughness Неровность
Magnetic Lasso Tool Магнитное лассо
Frequency Частота
Use tablet pressure to change pen width Использовать нажим планшета для изменения ширины пера
# About ▸ Contributors / Models (credits, craftrules standards/contributors.md)
About О программе
Contributors Участники
Models Модели
Sort Сортировка
Username Имя пользователя
Display name Отображаемое имя
Real name Настоящее имя
Reverse the order Обратный порядок
Grab bag Списком
Table Таблица
Name (A–Z) Имя (А–Я)
Merged PRs Принятые PR
Commits Коммиты
Lines added Добавлено строк
Lines deleted Удалено строк
Line delta Разница строк
Binary assets added Добавлено двоичных файлов
Binary assets removed Удалено двоичных файлов
First commit Первый коммит
First Первый
Last commit Последний коммит
Last Последний
Contributors: {contributors} · Commits: {commits} Участники: {contributors} · Коммиты: {commits}
No contributor data was built into this copy. В эту копию не встроены данные об участниках.
No model credits were built into this copy. В эту копию не встроены сведения о моделях.
Company Компания
Version Версия
% of all commits % от всех коммитов
Lines +/− Строки +/−
@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last} @{login}: PR: {prs}, коммиты: {commits}, строки +{added} / −{deleted} (Δ {delta}), двоичные файлы +{binAdded} / −{binDeleted}, {first} – {last}

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -2102,3 +2102,36 @@ cameraRaw Blending 混合
Magnetic Lasso Tool 磁性套索工具
Frequency 频率
Use tablet pressure to change pen width 使用绘图板压力以更改钢笔宽度
# About ▸ Contributors / Models (credits, craftrules standards/contributors.md)
About 关于
Contributors 贡献者
Models 模型
Sort 排序
Username 用户名
Display name 显示名称
Real name 真实姓名
Reverse the order 反转顺序
Grab bag 列表
Table 表格
Name (A–Z) 名称 (A–Z)
Merged PRs 已合并的 PR
Commits 提交
Lines added 新增行数
Lines deleted 删除行数
Line delta 行数增减
Binary assets added 新增二进制文件
Binary assets removed 删除二进制文件
First commit 首次提交
First 首次
Last commit 最近提交
Last 最近
Contributors: {contributors} · Commits: {commits} 贡献者:{contributors} · 提交:{commits}
No contributor data was built into this copy. 此副本未内置贡献者数据。
No model credits were built into this copy. 此副本未内置模型致谢。
Company 公司
Model 模型
Version 版本
% of all commits 占全部提交的 %
Lines +/− 行数 +/−
@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last} @{login}:{prs} 个 PR,{commits} 次提交,行 +{added} / −{deleted}(Δ {delta}),二进制文件 +{binAdded} / −{binDeleted},{first} – {last}

Can't render this file because it has a wrong number of fields in line 14.

View File

@ -2071,3 +2071,36 @@ cameraRaw Blending 混合
Magnetic Lasso Tool 磁性套索工具
Frequency 頻率
Use tablet pressure to change pen width 使用繪圖板壓力以變更鋼筆寬度
# About ▸ Contributors / Models (credits, craftrules standards/contributors.md)
About 關於
Contributors 貢獻者
Models 模型
Sort 排序
Username 使用者名稱
Display name 顯示名稱
Real name 真實姓名
Reverse the order 反轉順序
Grab bag 清單
Table 表格
Name (A–Z) 名稱 (A–Z)
Merged PRs 已合併的 PR
Commits 提交
Lines added 新增行數
Lines deleted 刪除行數
Line delta 行數增減
Binary assets added 新增二進位檔
Binary assets removed 刪除二進位檔
First commit 首次提交
First 首次
Last commit 最近提交
Last 最近
Contributors: {contributors} · Commits: {commits} 貢獻者:{contributors} · 提交:{commits}
No contributor data was built into this copy. 此副本未內建貢獻者資料。
No model credits were built into this copy. 此副本未內建模型致謝。
Company 公司
Model 模型
Version 版本
% of all commits 佔全部提交的 %
Lines +/− 行數 +/−
@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last} @{login}:{prs} 個 PR,{commits} 次提交,行 +{added} / −{deleted}(Δ {delta}),二進位檔 +{binAdded} / −{binDeleted},{first} – {last}

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -41,6 +41,7 @@ pub mod color_picker_ui;
pub mod color_range_ui;
pub mod comps_ui;
pub mod control;
pub mod credits;
pub mod crop_ui;
pub mod dialogs;
pub mod discard_ui;

57
docs/contributors.md Normal file
View File

@ -0,0 +1,57 @@
# Contributors in the About window
**About ▸ Contributors** credits everyone who contributed to PhotoCraft, and **About ▸ Models** credits
the AI models named in `Co-Authored-By` trailers. This follows the shared craftrules standard
[`standards/contributors.md`](https://github.com/storytold/craftrules/blob/main/standards/contributors.md);
this page is the local copy of the decision.
## Decision
- Two files, both **compiled into the binary** by the UI crate's `build.rs` (nothing is read from
disk or the network at run time; the web build has them too):
- [`contributors/contributors.json`](../contributors/contributors.json): commit stats per GitHub
username. **Generated** by `craftrules/scripts/contributors.py`; never hand-edit.
- [`contributors/people.toml`](../contributors/people.toml): the names people chose for
themselves. **Each contributor adds only their own entry**, in their own PR.
- Each contributor gets one line: GitHub username (always), display name (opt-in, or their synced
GitHub profile name), real name (only if they told us), merged PRs, commits, lines added, lines
deleted, binary assets added, binary assets removed, first and last commit date.
- The list is shown as a **grab bag** (names flowing as text) or a **table**, in the same order. It
sorts by name, PRs, commits, lines added, lines deleted, line delta, binary assets added/removed,
first or last commit date. The name toggle cycles **Username → Display name → Real name**; a
missing name falls back to `@username`. Alphabetical sorting is case-insensitive and ignores the `@`.
## In PhotoCraft
- The About window is `DialogKind::About` in `crates/ui-egui/src/dialogs.rs` (Help ▸ About PhotoCraft).
Its tab is the dialog field `tab` (`about`, `contributors`, `models`), so agents can switch it with
`ui.dialog.set`. Labels are translated like the rest of the shell (`tl!` + `crates/ui-egui/src/i18n/*.tsv`).
## Credit yourself
Add your entry to `contributors/people.toml` in a PR you commit yourself (every field optional):
```toml
[people.your-github-username]
real_name = "Your Name"
display_name = "Nickname"
sync_github_name = true # use the name on your public GitHub profile as display name
```
or, with craftrules checked out next to `craft-apps/`:
```sh
python3 ../../craftrules/scripts/contributors.py --add-me . --real-name "Your Name" --sync-github-name
```
Names show after the next refresh: the script checks that each entry was committed by its own
user (or by a maintainer with a `consent_source`). Delete your entry to remove your names.
## Refreshing (maintainers)
```sh
python3 ../../craftrules/scripts/contributors.py . # regenerate + verify; needs git, Python 3.11+, gh
python3 ../../craftrules/scripts/contributors.py --check . # verify people.toml only
```
Commit the updated `contributors/contributors.json`. Git author names and emails are never recorded.