Files
james.maesandClaude Opus 5.5 839fb72bec chore: adopt the KofTwentyTwo standards (#2)
## What

Brings AppKit into conformance with
[KofTwentyTwo/standards](https://github.com/KofTwentyTwo/standards).
Reviewable commit by commit:

| Commit | Change |
| --- | --- |
| build | Quality gate from `configs/dotnet` (analyzers incl. Meziantou,
warnings as errors), `packages.lock.json` per project, locked-mode
restore |
| style | Kingsrook layout and LF line endings, produced mechanically by
`dotnet format` (formatting only) |
| docs | SPDX license header on every C# file |
| refactor | Real code changes: analyzer findings fixed (one type per
file, regex timeouts, async void, …) and header comments on every type
and member |
| ci | Old workflows replaced by callers of the shared standards
workflows (PR gates, security, CodeQL, .NET CI, NuGet release with SBOM
+ attestation + trusted publishing), Scorecard |
| docs | README, CONTRIBUTING (GitHub Flow, DCO, signing), SECURITY,
CODEOWNERS, templates, Dependabot, threat model, ADRs, RELEASING |

Also includes `dev`'s CI fix, so it supersedes #1; the `dev` branch goes
away under GitHub Flow.

## Why

AppKit is the first Product repository to adopt the standards. Static
conformance before: 16 FAIL; after: 0 FAIL, 0 WARN (the remaining
UNKNOWN rows are GitHub settings and rulesets, applied separately).

## How tested

Locally with `CI=true` (lock files enforced): locked restore x64 and
ARM64; zero-warning x64 build; 115 unit tests with 100% line coverage of
`KofTwentyTwo.AppKit` and `KofTwentyTwo.AppKit.Updates`; 12 FlaUI UI
tests (WinUI + WPF samples); `dotnet format --verify-no-changes
--severity warn` clean; pack produces 4 packages + symbols; actionlint,
zizmor (pedantic), gitleaks clean.

The UI tests caught a regression introduced while fixing MA0147 (the WPF
splash stopped dismissing because no UI context existed yet); fixed by
starting the countdown through the dispatcher in both packages.

## Notes

- `ci.yml` and `release.yml` are pinned to KofTwentyTwo/standards#3's
head commit; they are repinned after that PR merges.
- This pull request's new check names become required checks once the
repository's rulesets are updated.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:43:17 -05:00

152 lines
5.8 KiB
C#

/*
* Copyright (c) 2026 James Maes (KofTwentyTwo)
* SPDX-License-Identifier: MIT
*/
using System.Globalization;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using KofTwentyTwo.AppKit;
namespace BrandTool;
/// <summary>
/// brandtool --out &lt;dir&gt; [--start #RRGGBB] [--end #RRGGBB] [--glyph "&lt;svg path data&gt;"] [--stroke 56]
/// Writes app-icon.svg and app.ico (16..256 px PNG frames) into &lt;dir&gt;.
/// </summary>
internal static class Program
{
private const double Box = 512;
private const double TileInset = 48;
private const double TileRadius = 104;
private static readonly int[] s_iconSizes = [16, 20, 24, 32, 40, 48, 64, 128, 256];
/// <summary>Entry point: reads the options, then writes app-icon.svg and a multi-size app.ico to the output folder.</summary>
[STAThread]
private static int Main(string[] args)
{
Dictionary<string, string> options = Parse(args);
if(!options.TryGetValue("out", out string? outDir))
{
Console.Error.WriteLine("usage: brandtool --out <dir> [--start #RRGGBB] [--end #RRGGBB] [--glyph \"<path data>\"] [--stroke <width>]");
return 2;
}
var spec = new MarkSpec(
options.GetValueOrDefault("start", "#3B4CCA"),
options.GetValueOrDefault("end", "#2A2F8F"),
options.GetValueOrDefault("glyph", "M 200 152 L 312 256 L 200 360"),
double.Parse(options.GetValueOrDefault("stroke", "56"), CultureInfo.InvariantCulture));
Directory.CreateDirectory(outDir);
File.WriteAllText(Path.Combine(outDir, "app-icon.svg"), Svg(spec));
WriteIco(Path.Combine(outDir, "app.ico"), s_iconSizes.Select(size => Png(spec, size)).ToList(), s_iconSizes);
Console.WriteLine($"Wrote app-icon.svg and app.ico to {Path.GetFullPath(outDir)}");
return 0;
}
/// <summary>Parses --name value pairs into a case-insensitive dictionary.</summary>
private static Dictionary<string, string> Parse(string[] args)
{
var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
for(int i = 0; i + 1 < args.Length; i += 2)
{
options[args[i].TrimStart('-')] = args[i + 1];
}
return options;
}
/// <summary>The mark as SVG text: the rounded gradient tile and the white stroked glyph.</summary>
private static string Svg(MarkSpec spec)
=> $"""
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<!-- Generated by AppKit BrandTool. Transparent outside the rounded tile. -->
<defs>
<linearGradient id="tile" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="{spec.Start}" />
<stop offset="1" stop-color="{spec.End}" />
</linearGradient>
</defs>
<rect x="{TileInset}" y="{TileInset}" width="{Box - 2 * TileInset}" height="{Box - 2 * TileInset}" rx="{TileRadius}" fill="url(#tile)" />
<path d="{spec.Glyph}" fill="none" stroke="#FFFFFF" stroke-width="{spec.Stroke.ToString(CultureInfo.InvariantCulture)}" stroke-linecap="round" stroke-linejoin="round" />
</svg>
""";
/// <summary>Renders the mark with WPF as a PNG of the given pixel size.</summary>
private static byte[] Png(MarkSpec spec, int size)
{
var visual = new DrawingVisual();
using(DrawingContext context = visual.RenderOpen())
{
double scale = size / Box;
context.PushTransform(new ScaleTransform(scale, scale));
var gradient = new LinearGradientBrush(ToColor(spec.Start), ToColor(spec.End), new Point(0, 0), new Point(1, 1));
context.DrawRoundedRectangle(gradient, null, new Rect(TileInset, TileInset, Box - 2 * TileInset, Box - 2 * TileInset), TileRadius, TileRadius);
var pen = new Pen(Brushes.White, spec.Stroke) { StartLineCap = PenLineCap.Round, EndLineCap = PenLineCap.Round, LineJoin = PenLineJoin.Round };
context.DrawGeometry(null, pen, Geometry.Parse(spec.Glyph));
}
var bitmap = new RenderTargetBitmap(size, size, 96, 96, PixelFormats.Pbgra32);
bitmap.Render(visual);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using var stream = new MemoryStream();
encoder.Save(stream);
return stream.ToArray();
}
/// <summary>Writes an .ico whose frames are embedded PNGs (supported since Windows Vista).</summary>
private static void WriteIco(string path, List<byte[]> pngs, int[] sizes)
{
using var writer = new BinaryWriter(File.Create(path));
writer.Write((ushort)0); // reserved
writer.Write((ushort)1); // type: icon
writer.Write((ushort)pngs.Count);
int offset = 6 + 16 * pngs.Count;
for(int i = 0; i < pngs.Count; i++)
{
writer.Write((byte)(sizes[i] >= 256 ? 0 : sizes[i])); // 0 means 256
writer.Write((byte)(sizes[i] >= 256 ? 0 : sizes[i]));
writer.Write((byte)0); // palette
writer.Write((byte)0); // reserved
writer.Write((ushort)1); // planes
writer.Write((ushort)32); // bits per pixel
writer.Write(pngs[i].Length);
writer.Write(offset);
offset += pngs[i].Length;
}
foreach(byte[] png in pngs)
{
writer.Write(png);
}
}
/// <summary>Parses a #RRGGBB or #AARRGGBB string into a WPF color.</summary>
private static Color ToColor(string hex)
{
var color = ArgbColor.Parse(hex);
return Color.FromArgb(color.A, color.R, color.G, color.B);
}
/// <summary>The mark definition: gradient colors, the glyph's SVG path data, and its stroke width.</summary>
private sealed record MarkSpec(string Start, string End, string Glyph, double Stroke);
}