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

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

Help > About VectorCraft gains Contributors and Models tabs next to the existing About
content, 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 (credits.rs). Each contributor
line: @username, opt-in display/real name, PRs, commits, lines +/-/delta, binary assets +/-,
first/last commit. Sortable by every column; alphabetical ignores case and the leading @.
Labels are translated (ja, zh-hant, pt-br catalog rows).

Co-Authored-By: Claude Opus 5.5 <[email protected]>

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* 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 <[email protected]>

* Contributors: regenerate (verified people.toml entries)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Contributors: regenerate after rebase onto main

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Contributors: regenerate (verified people.toml entries)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 authored and GitHub committed 2026-10-07 21:22:04 -04:00
1 parent fe9f3afcb7
commit f7a03160d6
16 files changed
+1197 -23

No files matched your search

+18
View File
@@ -55,3 +55,21 @@ Formerly **DrawCraft** (renamed 2026-10-01): old `.drawcraft` files and `"format
- When a task lands, update it in the same PR: the milestone row, the parity-table row (score, missing items, hours), "Shipped so far", and the gap list if the gap closed or shrank.
- Grade by behaviour against `plan/illustrator/`, not by whether a menu item exists. Scores are self-assessed, so err low.
- Keep the README's Status section in step with the ROADMAP headline.
## 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).
Generated
+1
View File
@@ -4690,6 +4690,7 @@ dependencies = [
"log",
"serde",
"serde_json",
"toml",
"vectorcraft-cad",
"vectorcraft-color",
"vectorcraft-doc",
+348
View File
@@ -0,0 +1,348 @@
{
"schema": 1,
"repo": "storytold/vectorcraft",
"branch": "main",
"head": "fe9f3afcb7d32032e8f332b0594692a03e275b84",
"head_date": "2026-10-08T00:36:16Z",
"commits": 607,
"unattributed_commits": 0,
"contributors": [
{
"login": "echelon",
"people_entry": true,
"github_name": null,
"prs": 15,
"commits": 119,
"lines_added": 172276,
"lines_deleted": 40329,
"binary_added": 37,
"binary_deleted": 2,
"first_commit": "2026-09-30T09:12:46Z",
"last_commit": "2026-10-07T22:04:30Z"
},
{
"login": "bflatastic",
"people_entry": false,
"github_name": null,
"prs": 176,
"commits": 394,
"lines_added": 212929,
"lines_deleted": 24160,
"binary_added": 2,
"binary_deleted": 0,
"first_commit": "2026-10-02T15:22:29Z",
"last_commit": "2026-10-08T00:36:16Z"
},
{
"login": "storybored379",
"people_entry": false,
"github_name": null,
"prs": 7,
"commits": 7,
"lines_added": 456,
"lines_deleted": 49,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-04T06:45:57Z",
"last_commit": "2026-10-04T08:14:20Z"
},
{
"login": "theironchef",
"people_entry": false,
"github_name": null,
"prs": 38,
"commits": 41,
"lines_added": 2803,
"lines_deleted": 232,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-05T14:40:20Z",
"last_commit": "2026-10-07T22:39:00Z"
},
{
"login": "midasdf",
"people_entry": false,
"github_name": null,
"prs": 2,
"commits": 2,
"lines_added": 579,
"lines_deleted": 43,
"binary_added": 1,
"binary_deleted": 0,
"first_commit": "2026-10-05T16:10:37Z",
"last_commit": "2026-10-07T20:53:30Z"
},
{
"login": "japbcoelho",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 57,
"lines_deleted": 13,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-06T01:45:49Z",
"last_commit": "2026-10-06T01:45:49Z"
},
{
"login": "p0lyg0n",
"people_entry": false,
"github_name": null,
"prs": 5,
"commits": 6,
"lines_added": 780,
"lines_deleted": 69,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-06T09:22:46Z",
"last_commit": "2026-10-07T04:33:45Z"
},
{
"login": "imachus",
"people_entry": false,
"github_name": null,
"prs": 12,
"commits": 12,
"lines_added": 6302,
"lines_deleted": 212,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-06T10:06:18Z",
"last_commit": "2026-10-07T22:20:10Z"
},
{
"login": "petrleocompel",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 784,
"lines_deleted": 36,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-06T12:23:11Z",
"last_commit": "2026-10-06T12:23:11Z"
},
{
"login": "fahrim",
"people_entry": false,
"github_name": null,
"prs": 3,
"commits": 6,
"lines_added": 1964,
"lines_deleted": 76,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-06T12:48:28Z",
"last_commit": "2026-10-07T23:36:17Z"
},
{
"login": "EthanSuquet",
"people_entry": false,
"github_name": null,
"prs": 4,
"commits": 4,
"lines_added": 413,
"lines_deleted": 71,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-06T19:30:49Z",
"last_commit": "2026-10-07T14:24:14Z"
},
{
"login": "realwitt",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 607,
"lines_deleted": 3,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T00:12:52Z",
"last_commit": "2026-10-07T00:12:52Z"
},
{
"login": "WildfootW",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 6809,
"lines_deleted": 2627,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T00:37:02Z",
"last_commit": "2026-10-07T00:37:02Z"
},
{
"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-07T02:29:03Z",
"last_commit": "2026-10-07T02:29:03Z"
},
{
"login": "Luskish",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 261,
"lines_deleted": 32,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T02:50:35Z",
"last_commit": "2026-10-07T02:50:35Z"
},
{
"login": "daniel-efrat",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 547,
"lines_deleted": 43,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T08:13:19Z",
"last_commit": "2026-10-07T08:13:19Z"
},
{
"login": "Pengjingyu1992",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 3132,
"lines_deleted": 4,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T09:56:23Z",
"last_commit": "2026-10-07T09:56:23Z"
},
{
"login": "MillhioreBT",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 564,
"lines_deleted": 41,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T12:58:33Z",
"last_commit": "2026-10-07T12:58:33Z"
},
{
"login": "fabian-co",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 2,
"lines_added": 270,
"lines_deleted": 48,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T18:05:26Z",
"last_commit": "2026-10-07T19:32:11Z"
},
{
"login": "Haven-King",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 2,
"lines_added": 467,
"lines_deleted": 76,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T18:10:39Z",
"last_commit": "2026-10-07T18:40:49Z"
},
{
"login": "njmueller",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 619,
"lines_deleted": 11,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T21:14:42Z",
"last_commit": "2026-10-07T21:14:42Z"
},
{
"login": "RicSchonfelder",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 2592,
"lines_deleted": 9,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T21:44:42Z",
"last_commit": "2026-10-07T21:44:42Z"
},
{
"login": "DWAA1660",
"people_entry": false,
"github_name": null,
"prs": 1,
"commits": 1,
"lines_added": 76,
"lines_deleted": 21,
"binary_added": 0,
"binary_deleted": 0,
"first_commit": "2026-10-07T23:09:37Z",
"last_commit": "2026-10-07T23:09:37Z"
}
],
"models": [
{
"id": "anthropic/claude-opus/5.5",
"company": "Anthropic",
"model": "Claude Opus",
"version": "5.5",
"commits": 567,
"lines_added": 382239,
"lines_deleted": 67174
},
{
"id": "anthropic/claude-sonnet/5.5",
"company": "Anthropic",
"model": "Claude Sonnet",
"version": "5.5",
"commits": 6,
"lines_added": 660,
"lines_deleted": 34
},
{
"id": "anthropic/claude-opus/4.8",
"company": "Anthropic",
"model": "Claude Opus",
"version": "4.8",
"commits": 3,
"lines_added": 1672,
"lines_deleted": 42
},
{
"id": "anthropic/claude-fable/5.1",
"company": "Anthropic",
"model": "Claude Fable",
"version": "5.1",
"commits": 1,
"lines_added": 6809,
"lines_deleted": 2627
}
]
}
+23
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
+4
View File
@@ -33,3 +33,7 @@ log = { workspace = true }
[dev-dependencies]
vectorcraft-testkit = { workspace = true }
wat = { workspace = true }
[build-dependencies]
serde_json = { workspace = true }
toml = "1"
+105
View File
@@ -0,0 +1,105 @@
//! 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
}
+404
View File
@@ -0,0 +1,404 @@
//! 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.
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 (the short count headers stay as they are).
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 {
format!(
"@{}: {} PRs, {} commits, +{} / −{} lines (Δ {}), +{} / −{} binary assets, {} – {}",
self.login,
self.prs,
self.commits,
group(self.lines_added),
group(self.lines_deleted),
signed(self.lines_delta()),
self.binary_added,
self.binary_deleted,
day(self.first_commit),
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) }
}
/// The About window doesn't resize: long lists scroll inside it.
const SCROLL_HEIGHT: f32 = 340.0;
#[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 (people, commits) = (group(list.len() as u64), group(TOTAL_COMMITS));
let counts = crate::i18n::fmt(tl!("{people} contributors · {commits} commits"), &[("people", &people), ("commits", &commits)]);
ui.label(RichText::new(counts).small().weak());
ui.separator();
egui::ScrollArea::both().max_height(SCROLL_HEIGHT).auto_shrink([false, true]).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().max_height(SCROLL_HEIGHT).auto_shrink([false, true]).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(""), "—");
}
}
+51 -23
View File
@@ -16,32 +16,60 @@ pub(super) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
.frame(egui::Frame::window(&ctx.global_style()).inner_margin(egui::Margin::same(18)))
.show(ctx, |ui| {
ui.set_width(380.0);
// Tabs: About · Contributors · Models (the credits are compiled in, see `credits`).
let tab_id = egui::Id::new("about_tab");
let mut tab = ui.data_mut(|d| d.get_temp::<u8>(tab_id)).unwrap_or(0);
ui.horizontal(|ui| {
let (r, _) = ui.allocate_exact_size(egui::vec2(44.0, 44.0), egui::Sense::hover());
crate::brand::paint_mark(ui, r);
ui.vertical(|ui| {
ui.label(egui::RichText::new("VectorCraft").font(theme::semibold(22.0)));
ui.label(crate::i18n::fmt(
tl!("Version {version} — open-source vector illustration in pure Rust."),
&[("version", env!("CARGO_PKG_VERSION"))],
));
});
for (i, label) in [tl!("About"), tl!("Contributors"), tl!("Models")].into_iter().enumerate() {
let i = i as u8;
if ui.selectable_label(tab == i, label).clicked() {
tab = i;
}
}
});
ui.add_space(12.0);
crate::community::links(app, ui);
ui.add_space(12.0);
ui.label(
egui::RichText::new(tl!(
"Part of ArtCraft. MIT OR Apache-2.0. Fonts: Source Sans 3, Inter, JetBrains Mono (OFL). Icons: Lucide (ISC) + VectorCraft."
))
.size(11.0),
);
// The GPU the window renders with, for bug reports (Preferences › Performance picks it).
if let Some(adapter) = &app.graphics_adapter {
ui.add_space(6.0);
ui.label(egui::RichText::new(crate::i18n::fmt(tl!("Graphics: {adapter}"), &[("adapter", adapter.as_str())])).size(11.0));
ui.data_mut(|d| d.insert_temp(tab_id, tab));
ui.separator();
match tab {
1 => {
ui.set_width(660.0);
crate::credits::contributors_ui(ui);
}
2 => {
ui.set_width(660.0);
crate::credits::models_ui(ui);
}
_ => about_tab(app, ui),
}
});
app.ui.about = open;
}
/// The About tab: mark, version, community links, licences and the GPU in use.
fn about_tab(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
ui.set_width(380.0);
ui.horizontal(|ui| {
let (r, _) = ui.allocate_exact_size(egui::vec2(44.0, 44.0), egui::Sense::hover());
crate::brand::paint_mark(ui, r);
ui.vertical(|ui| {
ui.label(egui::RichText::new("VectorCraft").font(theme::semibold(22.0)));
ui.label(crate::i18n::fmt(
tl!("Version {version} — open-source vector illustration in pure Rust."),
&[("version", env!("CARGO_PKG_VERSION"))],
));
});
});
ui.add_space(12.0);
crate::community::links(app, ui);
ui.add_space(12.0);
ui.label(
egui::RichText::new(tl!(
"Part of ArtCraft. MIT OR Apache-2.0. Fonts: Source Sans 3, Inter, JetBrains Mono (OFL). Icons: Lucide (ISC) + VectorCraft."
))
.size(11.0),
);
// The GPU the window renders with, for bug reports (Preferences › Performance picks it).
if let Some(adapter) = &app.graphics_adapter {
ui.add_space(6.0);
ui.label(egui::RichText::new(crate::i18n::fmt(tl!("Graphics: {adapter}"), &[("adapter", adapter.as_str())])).size(11.0));
}
}
+31
View File
@@ -572,3 +572,34 @@
Collapse Panels to Icons Sbalit panely na ikony
Collapse to Icons Sbalit na ikony
Expand Panels Rozbalit panely
# About › Contributors / Models (credits.rs)
About O aplikaci
Contributors Přispěvatelé
Models Modely
Username Uživatelské jméno
Display name Zobrazované jméno
Real name Skutečné jméno
Sort Řadit
Reverse the order Obrátit pořadí
Grab bag Volný seznam
Table Tabulka
Name (A–Z) Jméno (A–Z)
Merged PRs Sloučené PR
Commits Commity
Lines added Přidané řádky
Lines deleted Odstraněné řádky
Line delta Rozdíl řádků
Binary assets added Přidané binární soubory
Binary assets removed Odstraněné binární soubory
First commit První commit
Last commit Poslední commit
First První
Last Poslední
{people} contributors · {commits} commits Přispěvatelé: {people} · 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 uvedení modelů.
Company Společnost
Version Verze
% of all commits % všech commitů
Lines +/− Řádky +/−
Can't render this file because it contains an unexpected character in line 2 and column 73.
+31
View File
@@ -2972,3 +2972,34 @@
# Leading model (Paragraph panel menu, East Asian options).
Top-to-Top Leading Interlineado de superior a superior
Bottom-to-Bottom Leading Interlineado de inferior a inferior
# About › Contributors / Models (credits.rs)
About Acerca de
Contributors Colaboradores
Models Modelos
Username Nombre de usuario
Display name Nombre visible
Real name Nombre real
Sort Ordenar
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 agregadas
Lines deleted Líneas eliminadas
Line delta Diferencia de líneas
Binary assets added Recursos binarios agregados
Binary assets removed Recursos binarios eliminados
First commit Primer commit
Last commit Último commit
First Primero
Last Último
{people} contributors · {commits} commits {people} colaboradores · {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 +/−
Can't render this file because it contains an unexpected character in line 2 and column 73.
+31
View File
@@ -2961,3 +2961,34 @@
Saved Selection 25 保存した選択範囲 25
Left-to-Right Paragraph Direction 左から右への段落方向
Right-to-Left Paragraph Direction 右から左への段落方向
# About › Contributors / Models (credits.rs)
About 情報
Contributors コントリビューター
Models モデル
Username ユーザー名
Display name 表示名
Real name 本名
Sort 並べ替え
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 最初のコミット
Last commit 最後のコミット
First 最初
Last 最後
{people} contributors · {commits} commits コントリビューター {people} 人 · コミット {commits} 件
No contributor data was built into this copy. このビルドには貢献者のデータが含まれていません。
No model credits were built into this copy. このビルドにはモデルのクレジットが含まれていません。
Company 企業
Version バージョン
% of all commits 全コミットに占める割合
Lines +/− 行数 +/−
Can't render this file because it contains an unexpected character in line 2 and column 73.
+31
View File
@@ -2580,3 +2580,34 @@
@plural {n} star|{n} stars {n} estrela|{n} estrelas
@plural {n} step|{n} steps {n} passo|{n} passos
@plural {n} stroke|{n} strokes {n} traçado|{n} traçados
# About › Contributors / Models (credits.rs)
About Sobre
Contributors Colaboradores
Models Modelos
Username Nome de usuário
Display name Nome de exibição
Real name Nome real
Sort Ordenar
Reverse the order Inverter a ordem
Grab bag Todos juntos
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
Last commit Último commit
First Primeiro
Last Último
{people} contributors · {commits} commits {people} colaboradores · {commits} commits
No contributor data was built into this copy. Esta cópia foi compilada sem dados de colaboradores.
No model credits were built into this copy. Esta cópia foi compilada sem créditos de modelos.
Company Empresa
Version Versão
% of all commits % de todos os commits
Lines +/− Linhas +/−
Can't render this file because it contains an unexpected character in line 6 and column 47.
+31
View File
@@ -3219,3 +3219,34 @@
Saved Selection 23 已保存的选区 23
Saved Selection 24 已保存的选区 24
Saved Selection 25 已保存的选区 25
# About › Contributors / Models (credits.rs)
About 关于
Contributors 贡献者
Models 模型
Username 用户名
Display name 显示名称
Real name 真实姓名
Sort 排序
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 首次提交
Last commit 最近提交
First 首次
Last 最近
{people} contributors · {commits} commits {people} 位贡献者 · {commits} 次提交
No contributor data was built into this copy. 此版本未包含贡献者数据。
No model credits were built into this copy. 此版本未包含模型致谢。
Company 公司
Version 版本
% of all commits 占全部提交的百分比
Lines +/− 行数 +/−
Can't render this file because it contains an unexpected character in line 2 and column 73.
+31
View File
@@ -2992,3 +2992,34 @@
— {n} characters from another font — {n} 個字符來自其他字體
[Normal Character Style] [正常字元樣式]
[Normal Paragraph Style] [正常段落樣式]
# About › Contributors / Models (credits.rs)
About 關於
Contributors 貢獻者
Models 模型
Username 使用者名稱
Display name 顯示名稱
Real name 真實姓名
Sort 排序
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 首次提交
Last commit 最近一次提交
First 首次
Last 最近
{people} contributors · {commits} commits {people} 位貢獻者 · {commits} 次提交
No contributor data was built into this copy. 此版本未內建貢獻者資料。
No model credits were built into this copy. 此版本未內建模型致謝資料。
Company 公司
Version 版本
% of all commits 佔全部提交的比例
Lines +/− 行數 +/−
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -19,6 +19,7 @@ pub mod canvas;
pub mod chrome;
pub mod community;
pub mod control;
pub mod credits;
pub mod cursors;
pub mod dialogs;
pub mod dock;
+56
View File
@@ -0,0 +1,56 @@
# Contributors in the About window
**About ▸ Contributors** credits everyone who contributed to VectorCraft, 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 VectorCraft
- The About window is `crates/ui-egui/src/dialogs/about.rs` (Help ▸ About VectorCraft). The labels are
translated like the rest of the UI (`tl!`, `crates/ui-egui/src/i18n`); names and numbers are data.
## 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.