mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
About: contributor and model credits compiled in (grab bag + table, self-submitted names) (#108)
* About: contributors compiled in from contributors.json (grab bag + table, sortable, name toggle)
Adds the craftrules standard (standards/contributors.md) to Help > About DesignCraft, which now
has tabs About · Contributors · Models (`help.about {tab}` selects one).
contributors/contributors.json is generated by craftrules/scripts/contributors.py and baked
into the binary by crates/ui-egui/build.rs as static tables (crates/ui-egui/src/credits.rs).
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 @.
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>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f17397c305
commit
927191d25f
@@ -39,3 +39,21 @@ People trust DesignCraft with their layouts; a crash loses their work. **This ou
|
|||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
`ROADMAP.md` (committed) tracks status, milestones and estimates. Update it whenever a milestone task lands.
|
`ROADMAP.md` (committed) tracks status, milestones and estimates. Update it whenever a milestone task lands.
|
||||||
|
|
||||||
|
## 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
@@ -1111,6 +1111,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"toml",
|
||||||
"unicode-bidi",
|
"unicode-bidi",
|
||||||
"unicode-bidi-mirroring",
|
"unicode-bidi-mirroring",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
{
|
||||||
|
"schema": 1,
|
||||||
|
"repo": "storytold/designcraft",
|
||||||
|
"branch": "main",
|
||||||
|
"head": "f17397c305ecda9d644105f7a4e9b9d65d34bb60",
|
||||||
|
"head_date": "2026-10-07T18:55:09Z",
|
||||||
|
"commits": 294,
|
||||||
|
"unattributed_commits": 0,
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"login": "echelon",
|
||||||
|
"people_entry": true,
|
||||||
|
"github_name": null,
|
||||||
|
"prs": 22,
|
||||||
|
"commits": 260,
|
||||||
|
"lines_added": 99427,
|
||||||
|
"lines_deleted": 6061,
|
||||||
|
"binary_added": 42,
|
||||||
|
"binary_deleted": 2,
|
||||||
|
"first_commit": "2026-10-01T13:00:02Z",
|
||||||
|
"last_commit": "2026-10-07T18:55:09Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "storybored379",
|
||||||
|
"people_entry": false,
|
||||||
|
"github_name": null,
|
||||||
|
"prs": 4,
|
||||||
|
"commits": 4,
|
||||||
|
"lines_added": 150,
|
||||||
|
"lines_deleted": 11,
|
||||||
|
"binary_added": 0,
|
||||||
|
"binary_deleted": 0,
|
||||||
|
"first_commit": "2026-10-03T16:31:40Z",
|
||||||
|
"last_commit": "2026-10-05T03:53:26Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "CameronGilroy",
|
||||||
|
"people_entry": false,
|
||||||
|
"github_name": null,
|
||||||
|
"prs": 1,
|
||||||
|
"commits": 18,
|
||||||
|
"lines_added": 5236,
|
||||||
|
"lines_deleted": 381,
|
||||||
|
"binary_added": 0,
|
||||||
|
"binary_deleted": 0,
|
||||||
|
"first_commit": "2026-10-05T06:15:59Z",
|
||||||
|
"last_commit": "2026-10-05T18:56:22Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "midasdf",
|
||||||
|
"people_entry": false,
|
||||||
|
"github_name": null,
|
||||||
|
"prs": 1,
|
||||||
|
"commits": 1,
|
||||||
|
"lines_added": 161,
|
||||||
|
"lines_deleted": 0,
|
||||||
|
"binary_added": 1,
|
||||||
|
"binary_deleted": 0,
|
||||||
|
"first_commit": "2026-10-05T15:01:53Z",
|
||||||
|
"last_commit": "2026-10-05T15:01:53Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "ShaoWenbinSaleh",
|
||||||
|
"people_entry": false,
|
||||||
|
"github_name": null,
|
||||||
|
"prs": 1,
|
||||||
|
"commits": 2,
|
||||||
|
"lines_added": 6020,
|
||||||
|
"lines_deleted": 1278,
|
||||||
|
"binary_added": 0,
|
||||||
|
"binary_deleted": 0,
|
||||||
|
"first_commit": "2026-10-05T19:15:57Z",
|
||||||
|
"last_commit": "2026-10-06T14:32:13Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "japbcoelho",
|
||||||
|
"people_entry": false,
|
||||||
|
"github_name": null,
|
||||||
|
"prs": 9,
|
||||||
|
"commits": 9,
|
||||||
|
"lines_added": 782,
|
||||||
|
"lines_deleted": 91,
|
||||||
|
"binary_added": 0,
|
||||||
|
"binary_deleted": 0,
|
||||||
|
"first_commit": "2026-10-06T01:43:44Z",
|
||||||
|
"last_commit": "2026-10-06T01:45:41Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "anthropic/claude-opus/5.5",
|
||||||
|
"company": "Anthropic",
|
||||||
|
"model": "Claude Opus",
|
||||||
|
"version": "5.5",
|
||||||
|
"commits": 266,
|
||||||
|
"lines_added": 83821,
|
||||||
|
"lines_deleted": 6091
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "anthropic/claude-opus/4.8",
|
||||||
|
"company": "Anthropic",
|
||||||
|
"model": "Claude Opus",
|
||||||
|
"version": "4.8",
|
||||||
|
"commits": 1,
|
||||||
|
"lines_added": 1633,
|
||||||
|
"lines_deleted": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "openai/codex",
|
||||||
|
"company": "OpenAI",
|
||||||
|
"model": "Codex",
|
||||||
|
"version": "",
|
||||||
|
"commits": 1,
|
||||||
|
"lines_added": 26,
|
||||||
|
"lines_deleted": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -29,3 +29,7 @@ log = { workspace = true }
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# Headless (offscreen wgpu) rendering of the whole window for UI screenshots: `examples/ui_shot.rs`.
|
# Headless (offscreen wgpu) rendering of the whole window for UI screenshots: `examples/ui_shot.rs`.
|
||||||
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
|
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
toml = "1"
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Help › About DesignCraft: the splash with the ArtCraft mark, version, and community links
|
//! Help › About DesignCraft: tabs About · Contributors · Models. About is the splash with the
|
||||||
//! (Discord first and largest), plus the other ArtCraft apps.
|
//! ArtCraft mark, version, and community links (Discord first and largest), plus the other ArtCraft
|
||||||
|
//! apps; Contributors and Models are the compiled-in credits (`crate::credits`).
|
||||||
|
|
||||||
use designcraft_engine::links;
|
use designcraft_engine::links;
|
||||||
use egui::{Color32, Rect, Sense, Stroke, pos2, vec2};
|
use egui::{Color32, Rect, Sense, Stroke, pos2, vec2};
|
||||||
@@ -26,6 +27,9 @@ pub const SIBLINGS: &[(&str, &str, &str)] = &[
|
|||||||
("effectcraft", "EffectCraft", "motion graphics and visual effects"),
|
("effectcraft", "EffectCraft", "motion graphics and visual effects"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// The About window's tabs, in `UiState::about_tab` order (`help.about {tab}` names them in lowercase).
|
||||||
|
pub const ABOUT_TABS: [&str; 3] = ["About", "Contributors", "Models"];
|
||||||
|
|
||||||
/// Paint the ArtCraft mark into `rect` (square), rasterised once per size and cached.
|
/// Paint the ArtCraft mark into `rect` (square), rasterised once per size and cached.
|
||||||
pub fn paint_mark(ui: &egui::Ui, rect: Rect, color: Color32) {
|
pub fn paint_mark(ui: &egui::Ui, rect: Rect, color: Color32) {
|
||||||
use designcraft_render::vello_cpu::{self, kurbo, peniko};
|
use designcraft_render::vello_cpu::{self, kurbo, peniko};
|
||||||
@@ -103,7 +107,34 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
|||||||
let t = Tokens::get(ctx);
|
let t = Tokens::get(ctx);
|
||||||
let mut open: Option<&'static str> = None;
|
let mut open: Option<&'static str> = None;
|
||||||
let resp = egui::Modal::new(egui::Id::new("about")).show(ctx, |ui| {
|
let resp = egui::Modal::new(egui::Id::new("about")).show(ctx, |ui| {
|
||||||
ui.set_width(460.0);
|
ui.set_width(if app.ui.about_tab == 0 { 460.0 } else { 640.0 });
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
for (i, l) in ABOUT_TABS.iter().enumerate() {
|
||||||
|
let i = i as u8;
|
||||||
|
if ui.selectable_label(app.ui.about_tab == i, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, l))).clicked() {
|
||||||
|
app.ui.about_tab = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
if app.ui.about_tab != 0 {
|
||||||
|
let h = 400.0_f32.min(ctx.content_rect().height() * 0.7).max(160.0);
|
||||||
|
ui.allocate_ui_with_layout(vec2(ui.available_width(), h), egui::Layout::top_down(egui::Align::Min), |ui| {
|
||||||
|
ui.set_min_height(h);
|
||||||
|
if app.ui.about_tab == 1 {
|
||||||
|
crate::credits::contributors_ui(ui);
|
||||||
|
} else {
|
||||||
|
crate::credits::models_ui(ui);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, " Close "))).clicked() {
|
||||||
|
app.ui.about = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
ui.vertical_centered(|ui| {
|
ui.vertical_centered(|ui| {
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
let (r, _) = ui.allocate_exact_size(vec2(72.0, 72.0), Sense::hover());
|
let (r, _) = ui.allocate_exact_size(vec2(72.0, 72.0), Sense::hover());
|
||||||
|
|||||||
@@ -0,0 +1,399 @@
|
|||||||
|
//! 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 => "Username",
|
||||||
|
NameMode::DisplayName => "Display name",
|
||||||
|
NameMode::RealName => "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.
|
||||||
|
pub fn label(self) -> (&'static str, &'static str) {
|
||||||
|
match self {
|
||||||
|
SortKey::Name => ("Name (A–Z)", "Name"),
|
||||||
|
SortKey::Prs => ("Merged PRs", "PRs"),
|
||||||
|
SortKey::Commits => ("Commits", "Commits"),
|
||||||
|
SortKey::LinesAdded => ("Lines added", "+LOC"),
|
||||||
|
SortKey::LinesDeleted => ("Lines deleted", "−LOC"),
|
||||||
|
SortKey::LinesDelta => ("Line delta", "ΔLOC"),
|
||||||
|
SortKey::BinaryAdded => ("Binary assets added", "+Bin"),
|
||||||
|
SortKey::BinaryDeleted => ("Binary assets removed", "−Bin"),
|
||||||
|
SortKey::FirstCommit => ("First commit", "First"),
|
||||||
|
SortKey::LastCommit => ("Last commit", "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) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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("Show");
|
||||||
|
for m in NameMode::ALL {
|
||||||
|
if ui.selectable_label(v.names == m, m.label()).clicked() {
|
||||||
|
v.names = m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
ui.label("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("Reverse the order").clicked() {
|
||||||
|
v.ascending = !v.ascending;
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
if ui.selectable_label(!v.table, "Grab bag").clicked() {
|
||||||
|
v.table = false;
|
||||||
|
}
|
||||||
|
if ui.selectable_label(v.table, "Table").clicked() {
|
||||||
|
v.table = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let list = sorted(CONTRIBUTORS, v.names, v.key, v.ascending);
|
||||||
|
ui.label(RichText::new(format!("{} contributors · {} commits", list.len(), group(TOTAL_COMMITS))).small().weak());
|
||||||
|
ui.separator();
|
||||||
|
egui::ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||||
|
if list.is_empty() {
|
||||||
|
ui.label("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("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]).show(ui, |ui| {
|
||||||
|
egui::Grid::new("credits_models").striped(true).num_columns(6).show(ui, |ui| {
|
||||||
|
for h in ["Company", "Model", "Version", "Commits", "% of all commits", "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(""), "—");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ pub mod about;
|
|||||||
pub mod canvas;
|
pub mod canvas;
|
||||||
pub mod chrome;
|
pub mod chrome;
|
||||||
pub mod control;
|
pub mod control;
|
||||||
|
pub mod credits;
|
||||||
pub mod dialogs;
|
pub mod dialogs;
|
||||||
pub mod dock;
|
pub mod dock;
|
||||||
pub mod i18n;
|
pub mod i18n;
|
||||||
@@ -190,6 +191,9 @@ pub struct UiState {
|
|||||||
pub task_bar: bool,
|
pub task_bar: bool,
|
||||||
/// Help › About DesignCraft is open.
|
/// Help › About DesignCraft is open.
|
||||||
pub about: bool,
|
pub about: bool,
|
||||||
|
/// The About window's tab: 0 About, 1 Contributors, 2 Models (`about::ABOUT_TABS`).
|
||||||
|
#[serde(skip)]
|
||||||
|
pub about_tab: u8,
|
||||||
/// URLs to open in the browser on the next frame (Help links, About, start screen).
|
/// URLs to open in the browser on the next frame (Help links, About, start screen).
|
||||||
pub pending_urls: Vec<String>,
|
pub pending_urls: Vec<String>,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
@@ -261,6 +265,7 @@ impl Default for UiState {
|
|||||||
snap_zone: 4.0,
|
snap_zone: 4.0,
|
||||||
task_bar: true,
|
task_bar: true,
|
||||||
about: false,
|
about: false,
|
||||||
|
about_tab: 0,
|
||||||
pending_urls: Vec::new(),
|
pending_urls: Vec::new(),
|
||||||
status: String::new(),
|
status: String::new(),
|
||||||
dialog: None,
|
dialog: None,
|
||||||
|
|||||||
@@ -135,7 +135,12 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
|
|||||||
("help.issues", "Report an Issue…", None, "{} — opens the GitHub issue tracker"),
|
("help.issues", "Report an Issue…", None, "{} — opens the GitHub issue tracker"),
|
||||||
("help.website", "ArtCraft Website…", None, "{} — opens https://getartcraft.com"),
|
("help.website", "ArtCraft Website…", None, "{} — opens https://getartcraft.com"),
|
||||||
("help.app", "ArtCraft App Page…", None, "{app} — opens https://getartcraft.com/apps/{app} (e.g. photocraft)"),
|
("help.app", "ArtCraft App Page…", None, "{app} — opens https://getartcraft.com/apps/{app} (e.g. photocraft)"),
|
||||||
("help.about", "About DesignCraft", None, "{open?: true} — the About splash with community links (open: false closes it)"),
|
(
|
||||||
|
"help.about",
|
||||||
|
"About DesignCraft",
|
||||||
|
None,
|
||||||
|
"{open?: true, tab?: \"about\"|\"contributors\"|\"models\"} — the About window: splash with community links, contributor and model credits (open: false closes it)",
|
||||||
|
),
|
||||||
("window.toolsDoubleColumn", "Tools: Double Column", None, "{}"),
|
("window.toolsDoubleColumn", "Tools: Double Column", None, "{}"),
|
||||||
(
|
(
|
||||||
"window.workspace",
|
"window.workspace",
|
||||||
@@ -1278,6 +1283,12 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
|||||||
}
|
}
|
||||||
"window.taskBar" => flag(&mut app.ui.task_bar),
|
"window.taskBar" => flag(&mut app.ui.task_bar),
|
||||||
"help.about" => {
|
"help.about" => {
|
||||||
|
if let Some(tab) = p.get("tab").and_then(Value::as_str) {
|
||||||
|
let Some(i) = crate::about::ABOUT_TABS.iter().position(|t| t.eq_ignore_ascii_case(tab)) else {
|
||||||
|
return Some(Err(format!("help.about: unknown tab `{tab}` (about, contributors, models)")));
|
||||||
|
};
|
||||||
|
app.ui.about_tab = u8::try_from(i).unwrap_or(0);
|
||||||
|
}
|
||||||
app.ui.about = p.get("open").and_then(Value::as_bool).unwrap_or(true);
|
app.ui.about = p.get("open").and_then(Value::as_bool).unwrap_or(true);
|
||||||
Ok(json!(app.ui.about))
|
Ok(json!(app.ui.about))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Contributors in the About window
|
||||||
|
|
||||||
|
**About ▸ Contributors** credits everyone who contributed to DesignCraft, 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 DesignCraft
|
||||||
|
|
||||||
|
- The About window is `crates/ui-egui/src/about.rs` (Help ▸ About DesignCraft); `help.about {"tab":"contributors"}` opens a tab.
|
||||||
|
|
||||||
|
## 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.
|
||||||
+1
-1
@@ -93,4 +93,4 @@ key {"key": "Escape"}
|
|||||||
|
|
||||||
## Community links
|
## Community links
|
||||||
|
|
||||||
`app.links` returns the ArtCraft Discord (https://discord.gg/artcraft), the website (https://getartcraft.com), DesignCraft's app page (https://getartcraft.com/apps/designcraft), its GitHub repository (https://github.com/storytold/designcraft) and the issue tracker. In the running app, the UI commands `help.discord`, `help.appPage`, `help.github`, `help.issues`, `help.website`, `help.app {app}` open them in the browser and `help.about` shows the About splash. The server's `serverInfo.websiteUrl` is the app page.
|
`app.links` returns the ArtCraft Discord (https://discord.gg/artcraft), the website (https://getartcraft.com), DesignCraft's app page (https://getartcraft.com/apps/designcraft), its GitHub repository (https://github.com/storytold/designcraft) and the issue tracker. In the running app, the UI commands `help.discord`, `help.appPage`, `help.github`, `help.issues`, `help.website`, `help.app {app}` open them in the browser and `help.about {tab?}` shows the About window (tabs `about`, `contributors`, `models`). The server's `serverInfo.websiteUrl` is the app page.
|
||||||
|
|||||||
Reference in New Issue
Block a user