diff --git a/ThemeProvider.Analysis/AnalysisModel.cs b/ThemeProvider.Analysis/AnalysisModel.cs
new file mode 100644
index 0000000..4ead0cc
--- /dev/null
+++ b/ThemeProvider.Analysis/AnalysisModel.cs
@@ -0,0 +1,69 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ThemeProvider.Analysis;
+
+using System.Collections.Generic;
+using System.Linq;
+
+/// The outcome of a single check.
+internal enum CheckStatus
+{
+ /// The check met its gate.
+ Pass,
+
+ /// The check is within a soft tolerance but worth attention.
+ Warn,
+
+ /// The check failed its gate.
+ Fail,
+}
+
+/// A single issue surfaced by a check (only warnings and failures are recorded).
+/// The check that produced the finding.
+/// The severity.
+/// A human-readable description with the measured value and gate.
+internal sealed record Finding(string Category, CheckStatus Status, string Message);
+
+/// The headline numbers shown in the per-theme summary row. Arrows in the field
+/// names indicate the direction that is worse (lower contrast, higher drift, etc.).
+/// Lowest text-over-surface contrast ratio.
+/// The foreground/background pair that produced it.
+/// Lowest UI-glyph contrast ratio.
+/// The foreground/background pair that produced it.
+/// Smallest Oklab lightness delta between adjacent interaction states.
+/// The interaction-state pair that produced it.
+/// Largest accent hue drift from the source palette, in degrees.
+/// Smallest accent chroma retention ratio.
+/// Semantic meanings the theme defines but the ImGui mapper never consumes.
+internal sealed record ThemeMetrics(
+ double MinTextContrast,
+ string MinTextPair,
+ double MinGlyphContrast,
+ string MinGlyphPair,
+ double MinStateDelta,
+ string MinStatePair,
+ double MaxHueDrift,
+ double MinChromaRetention,
+ IReadOnlyList UnusedMeanings);
+
+/// The full analysis result for one theme.
+/// The theme's display name.
+/// The theme family.
+/// Whether the theme is dark.
+/// The headline numbers.
+/// The warnings and failures.
+internal sealed record ThemeAnalysis(
+ string Name,
+ string Family,
+ bool IsDark,
+ ThemeMetrics Metrics,
+ IReadOnlyList Findings)
+{
+ /// The worst status across all findings.
+ public CheckStatus Status =>
+ Findings.Any(f => f.Status == CheckStatus.Fail) ? CheckStatus.Fail
+ : Findings.Any(f => f.Status == CheckStatus.Warn) ? CheckStatus.Warn
+ : CheckStatus.Pass;
+}
diff --git a/ThemeProvider.Analysis/AnalysisThresholds.cs b/ThemeProvider.Analysis/AnalysisThresholds.cs
new file mode 100644
index 0000000..38ee0ec
--- /dev/null
+++ b/ThemeProvider.Analysis/AnalysisThresholds.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ThemeProvider.Analysis;
+
+///
+/// The fixed gate values every theme is measured against. Tune them here; the report and the
+/// process exit code both derive from these. Contrast values are WCAG contrast ratios (1..21),
+/// lightness deltas are in Oklab L (0..1), hue drift is in degrees, chroma retention is a ratio.
+///
+internal static class AnalysisThresholds
+{
+ /// Minimum contrast for body text over the surfaces it is drawn on (WCAG AA, normal text).
+ public const double TextContrastAa = 4.5;
+
+ /// Floor for intentionally-muted disabled text: below this it reads as invisible rather than dimmed.
+ public const double DisabledTextFloor = 2.0;
+
+ /// Minimum contrast for UI glyphs such as the checkmark (WCAG 1.4.11 non-text contrast).
+ public const double GlyphContrast = 3.0;
+
+ /// Minimum Oklab lightness difference between adjacent interaction states to be perceptible.
+ public const double MinStateLightnessDelta = 0.015;
+
+ /// Maximum hue drift (degrees) an accent color may wander from the theme's source palette.
+ public const double MaxAccentHueDriftDegrees = 12.0;
+
+ /// Warn when a derived accent keeps less than this fraction of the source palette's chroma.
+ public const double MinAccentChromaRetention = 0.40;
+
+ /// Chroma below which a color is treated as neutral and its hue is not meaningful.
+ public const double MeaningfulChroma = 0.02;
+}
diff --git a/ThemeProvider.Analysis/MarkdownReport.cs b/ThemeProvider.Analysis/MarkdownReport.cs
new file mode 100644
index 0000000..f6af2ba
--- /dev/null
+++ b/ThemeProvider.Analysis/MarkdownReport.cs
@@ -0,0 +1,115 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ThemeProvider.Analysis;
+
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+/// Renders a set of theme analyses as a single deterministic markdown document.
+internal static class MarkdownReport
+{
+ /// Builds the markdown report.
+ /// The per-theme results, in the order they should appear.
+ /// The report text.
+ public static string Build(IReadOnlyList analyses)
+ {
+ StringBuilder sb = new();
+
+ int failed = analyses.Count(a => a.Status == CheckStatus.Fail);
+ int warned = analyses.Count(a => a.Status == CheckStatus.Warn);
+ int passed = analyses.Count - failed - warned;
+
+ sb.AppendLine("# Theme palette analysis");
+ sb.AppendLine();
+ sb.AppendLine(Inv($"{analyses.Count} themes analyzed: {passed} pass, {warned} warn, {failed} fail."));
+ sb.AppendLine();
+ AppendLegend(sb);
+
+ sb.AppendLine("## Summary");
+ sb.AppendLine();
+ sb.AppendLine("| Theme | Mode | Text | Glyph | State delta | Hue drift | Chroma | Status |");
+ sb.AppendLine("| --- | --- | --- | --- | --- | --- | --- | --- |");
+ foreach (ThemeAnalysis a in analyses)
+ {
+ ThemeMetrics m = a.Metrics;
+ string mode = a.IsDark ? "dark" : "light";
+ sb.AppendLine(Inv($"| {a.Name} | {mode} | {m.MinTextContrast:0.0}:1 | {m.MinGlyphContrast:0.0}:1 | {m.MinStateDelta:0.000} | {m.MaxHueDrift:0}deg | {m.MinChromaRetention * 100.0:0}% | {Label(a.Status)} |"));
+ }
+
+ sb.AppendLine();
+ sb.AppendLine("## Findings");
+ sb.AppendLine();
+
+ IReadOnlyList withFindings = [.. analyses.Where(a => a.Findings.Count > 0)];
+ if (withFindings.Count == 0)
+ {
+ sb.AppendLine("No warnings or failures.");
+ }
+ else
+ {
+ foreach (ThemeAnalysis a in withFindings)
+ {
+ sb.AppendLine(Inv($"### {a.Name} — {Label(a.Status)}"));
+ sb.AppendLine();
+ foreach (Finding f in a.Findings.OrderByDescending(f => f.Status))
+ {
+ sb.AppendLine(Inv($"- **{Label(f.Status)}** [{f.Category}] {f.Message}"));
+ }
+
+ sb.AppendLine();
+ }
+ }
+
+ AppendUnusedMeanings(sb, analyses);
+
+ return sb.ToString();
+ }
+
+ private static void AppendLegend(StringBuilder sb)
+ {
+ sb.AppendLine("## What is measured");
+ sb.AppendLine();
+ sb.AppendLine(Inv($"- **Text** — lowest WCAG contrast of body text over any surface it renders on. Gate: {AnalysisThresholds.TextContrastAa:0.0}:1 (AA)."));
+ sb.AppendLine(Inv($"- **Glyph** — lowest contrast of UI glyphs (checkmark). Gate: {AnalysisThresholds.GlyphContrast:0.0}:1 (WCAG 1.4.11)."));
+ sb.AppendLine(Inv($"- **State delta** — smallest Oklab lightness step between adjacent interaction states (button/frame hover/active). Gate: {AnalysisThresholds.MinStateLightnessDelta:0.000}."));
+ sb.AppendLine(Inv($"- **Hue drift** — largest angle an accent wanders from the theme's source palette. Gate: {AnalysisThresholds.MaxAccentHueDriftDegrees:0}deg."));
+ sb.AppendLine(Inv($"- **Chroma** — smallest fraction of source accent chroma retained. Warns below {AnalysisThresholds.MinAccentChromaRetention * 100.0:0}%."));
+ sb.AppendLine();
+ sb.AppendLine("Disabled text is checked against an invisibility floor only, since it is meant to read as muted.");
+ sb.AppendLine();
+ }
+
+ private static void AppendUnusedMeanings(StringBuilder sb, IReadOnlyList analyses)
+ {
+ IReadOnlyList withUnused = [.. analyses.Where(a => a.Metrics.UnusedMeanings.Count > 0)];
+ if (withUnused.Count == 0)
+ {
+ return;
+ }
+
+ sb.AppendLine("## Semantic meanings defined but unused by the ImGui mapper");
+ sb.AppendLine();
+ sb.AppendLine("These are computed by the theme but never mapped to an ImGui slot, so status colors (success/warning/error/etc.) are unavailable to applications through the mapper.");
+ sb.AppendLine();
+ foreach (ThemeAnalysis a in withUnused)
+ {
+ string meanings = string.Join(", ", a.Metrics.UnusedMeanings.Select(m => m.ToString()));
+ sb.AppendLine(Inv($"- {a.Name}: {meanings}"));
+ }
+
+ sb.AppendLine();
+ }
+
+ private static string Label(CheckStatus status) => status switch
+ {
+ CheckStatus.Fail => "FAIL",
+ CheckStatus.Warn => "WARN",
+ _ => "PASS",
+ };
+
+ private static string Inv(FormattableString text) => text.ToString(CultureInfo.InvariantCulture);
+}
diff --git a/ThemeProvider.Analysis/PaletteAnalyzer.cs b/ThemeProvider.Analysis/PaletteAnalyzer.cs
new file mode 100644
index 0000000..82e9e35
--- /dev/null
+++ b/ThemeProvider.Analysis/PaletteAnalyzer.cs
@@ -0,0 +1,281 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ThemeProvider.Analysis;
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using System.Numerics;
+
+using Hexa.NET.ImGui;
+
+using ktsu.Semantics.Color;
+using ktsu.ThemeProvider.ImGui;
+
+///
+/// Runs the four color checks against the actual ImGui palette a theme produces, plus the
+/// semantic palette it is derived from. Analyzes the real shipped output of
+/// so a change to the mapper is caught here too.
+///
+internal static class PaletteAnalyzer
+{
+ // The semantic meanings ImGuiPaletteMapper actually consumes. Kept in sync with that mapper by hand;
+ // meanings outside this set are reported as unused, and a missing member is reported as a coverage gap.
+ private static readonly SemanticMeaning[] MapperUsedMeanings =
+ [
+ SemanticMeaning.Neutral,
+ SemanticMeaning.Primary,
+ SemanticMeaning.Alternate,
+ ];
+
+ // Text-over-surface pairs. Body text is drawn on all of these; each must clear WCAG AA.
+ private static readonly (ImGuiCol Fg, ImGuiCol Bg)[] TextPairs =
+ [
+ (ImGuiCol.Text, ImGuiCol.WindowBg),
+ (ImGuiCol.Text, ImGuiCol.ChildBg),
+ (ImGuiCol.Text, ImGuiCol.PopupBg),
+ (ImGuiCol.Text, ImGuiCol.MenuBarBg),
+ (ImGuiCol.Text, ImGuiCol.FrameBg),
+ (ImGuiCol.Text, ImGuiCol.Button),
+ (ImGuiCol.Text, ImGuiCol.Header),
+ (ImGuiCol.Text, ImGuiCol.TitleBgActive),
+ (ImGuiCol.Text, ImGuiCol.Tab),
+ (ImGuiCol.Text, ImGuiCol.TabSelected),
+ (ImGuiCol.Text, ImGuiCol.TableHeaderBg),
+ ];
+
+ // Disabled text is intentionally muted; it only needs to stay above the invisibility floor.
+ private static readonly (ImGuiCol Fg, ImGuiCol Bg)[] DisabledTextPairs =
+ [
+ (ImGuiCol.TextDisabled, ImGuiCol.WindowBg),
+ (ImGuiCol.TextDisabled, ImGuiCol.FrameBg),
+ ];
+
+ // UI glyphs (the checkmark) drawn inside frames; WCAG 1.4.11 non-text contrast applies.
+ private static readonly (ImGuiCol Fg, ImGuiCol Bg)[] GlyphPairs =
+ [
+ (ImGuiCol.CheckMark, ImGuiCol.FrameBg),
+ (ImGuiCol.CheckMark, ImGuiCol.WindowBg),
+ ];
+
+ // Adjacent interaction states that must be visibly distinct.
+ private static readonly (ImGuiCol A, ImGuiCol B)[] StatePairs =
+ [
+ (ImGuiCol.Button, ImGuiCol.ButtonHovered),
+ (ImGuiCol.ButtonHovered, ImGuiCol.ButtonActive),
+ (ImGuiCol.FrameBg, ImGuiCol.FrameBgHovered),
+ (ImGuiCol.FrameBgHovered, ImGuiCol.FrameBgActive),
+ ];
+
+ // Accent meanings and the priorities the mapper draws from them, used for the fidelity check.
+ private static readonly (SemanticMeaning Meaning, Priority[] Priorities)[] AccentUsage =
+ [
+ (SemanticMeaning.Primary, [Priority.MediumLow, Priority.Medium, Priority.High, Priority.VeryHigh]),
+ (SemanticMeaning.Alternate, [Priority.Medium, Priority.High]),
+ ];
+
+ /// Analyzes one theme and returns its full result.
+ /// The theme to analyze.
+ /// The analysis, including headline metrics and any findings.
+ public static ThemeAnalysis Analyze(ThemeRegistry.ThemeInfo info)
+ {
+ Ensure.NotNull(info);
+
+ ISemanticTheme theme = info.CreateInstance();
+ IReadOnlyDictionary imgui = new ImGuiPaletteMapper().MapTheme(theme);
+ IReadOnlyDictionary semantic = SemanticColorMapper.MakeCompletePalette(theme);
+
+ List findings = [];
+
+ (double minTextContrast, string minTextPair) = CheckContrast(imgui, TextPairs, AnalysisThresholds.TextContrastAa, "Text contrast", findings);
+ CheckContrast(imgui, DisabledTextPairs, AnalysisThresholds.DisabledTextFloor, "Disabled text", findings);
+ (double minGlyphContrast, string minGlyphPair) = CheckContrast(imgui, GlyphPairs, AnalysisThresholds.GlyphContrast, "Glyph contrast", findings);
+ (double minStateDelta, string minStatePair) = CheckDistinctness(imgui, findings);
+ (double maxHueDrift, double minChromaRetention) = CheckFidelity(theme, semantic, findings);
+ IReadOnlyList unused = CheckCoverage(theme, findings);
+
+ ThemeMetrics metrics = new(
+ minTextContrast, minTextPair,
+ minGlyphContrast, minGlyphPair,
+ minStateDelta, minStatePair,
+ maxHueDrift, minChromaRetention,
+ unused);
+
+ return new ThemeAnalysis(info.Name, info.Family, info.IsDark, metrics, findings);
+ }
+
+ private static (double Min, string Pair) CheckContrast(
+ IReadOnlyDictionary imgui,
+ (ImGuiCol Fg, ImGuiCol Bg)[] pairs,
+ double gate,
+ string category,
+ List findings)
+ {
+ double min = double.PositiveInfinity;
+ string minPair = "n/a";
+
+ foreach ((ImGuiCol fg, ImGuiCol bg) in pairs)
+ {
+ if (ToColor(imgui, fg) is not Color fgColor || ToColor(imgui, bg) is not Color bgColor)
+ {
+ continue;
+ }
+
+ double ratio = fgColor.ContrastRatio(bgColor);
+ string pair = $"{fg}/{bg}";
+ if (ratio < min)
+ {
+ min = ratio;
+ minPair = pair;
+ }
+
+ if (ratio < gate)
+ {
+ findings.Add(new Finding(
+ category,
+ CheckStatus.Fail,
+ $"{pair} is {Num(ratio)}:1 (needs {Num(gate)}:1)"));
+ }
+ }
+
+ return (double.IsPositiveInfinity(min) ? 0.0 : min, minPair);
+ }
+
+ private static (double MinDelta, string Pair) CheckDistinctness(
+ IReadOnlyDictionary imgui,
+ List findings)
+ {
+ double min = double.PositiveInfinity;
+ string minPair = "n/a";
+
+ foreach ((ImGuiCol a, ImGuiCol b) in StatePairs)
+ {
+ if (ToColor(imgui, a) is not Color ca || ToColor(imgui, b) is not Color cb)
+ {
+ continue;
+ }
+
+ double delta = Math.Abs(ca.ToOklab().L - cb.ToOklab().L);
+ string pair = $"{a}->{b}";
+ if (delta < min)
+ {
+ min = delta;
+ minPair = pair;
+ }
+
+ if (delta < AnalysisThresholds.MinStateLightnessDelta)
+ {
+ findings.Add(new Finding(
+ "Interaction distinctness",
+ CheckStatus.Fail,
+ $"{pair} differ by only {Num(delta, "0.000")} Oklab L (needs {Num(AnalysisThresholds.MinStateLightnessDelta, "0.000")})"));
+ }
+ }
+
+ return (double.IsPositiveInfinity(min) ? 0.0 : min, minPair);
+ }
+
+ private static (double MaxHueDrift, double MinChromaRetention) CheckFidelity(
+ ISemanticTheme theme,
+ IReadOnlyDictionary semantic,
+ List findings)
+ {
+ double maxHueDrift = 0.0;
+ double minChromaRetention = double.PositiveInfinity;
+
+ foreach ((SemanticMeaning meaning, Priority[] priorities) in AccentUsage)
+ {
+ if (!theme.SemanticMapping.TryGetValue(meaning, out Collection? sourceColors) || sourceColors.Count == 0)
+ {
+ continue;
+ }
+
+ List sourceHued = [.. sourceColors
+ .Select(c => c.ToOklch())
+ .Where(o => o.C > AnalysisThresholds.MeaningfulChroma)];
+ double maxSourceChroma = sourceColors.Max(c => c.ToOklch().C);
+
+ foreach (Priority priority in priorities)
+ {
+ if (!semantic.TryGetValue(new SemanticColorRequest(meaning, priority), out Color derived))
+ {
+ continue;
+ }
+
+ Oklch d = derived.ToOklch();
+
+ if (maxSourceChroma > AnalysisThresholds.MeaningfulChroma)
+ {
+ double retention = d.C / maxSourceChroma;
+ if (retention < minChromaRetention)
+ {
+ minChromaRetention = retention;
+ }
+
+ if (retention < AnalysisThresholds.MinAccentChromaRetention)
+ {
+ findings.Add(new Finding(
+ "Canonical fidelity",
+ CheckStatus.Warn,
+ $"{meaning} {priority} keeps only {Num(retention * 100.0, "0")}% of source chroma"));
+ }
+ }
+
+ if (d.C > AnalysisThresholds.MeaningfulChroma && sourceHued.Count > 0)
+ {
+ double drift = sourceHued.Min(s => HueDelta(d.H, s.H));
+ if (drift > maxHueDrift)
+ {
+ maxHueDrift = drift;
+ }
+
+ if (drift > AnalysisThresholds.MaxAccentHueDriftDegrees)
+ {
+ findings.Add(new Finding(
+ "Canonical fidelity",
+ CheckStatus.Warn,
+ $"{meaning} {priority} hue drifts {Num(drift, "0")}deg from source (max {Num(AnalysisThresholds.MaxAccentHueDriftDegrees, "0")}deg)"));
+ }
+ }
+ }
+ }
+
+ return (maxHueDrift, double.IsPositiveInfinity(minChromaRetention) ? 1.0 : minChromaRetention);
+ }
+
+ private static IReadOnlyList CheckCoverage(ISemanticTheme theme, List findings)
+ {
+ HashSet provided = [.. theme.SemanticMapping.Keys];
+
+ foreach (SemanticMeaning required in MapperUsedMeanings)
+ {
+ if (!provided.Contains(required))
+ {
+ // Neutral and Primary underpin backgrounds and accents; Alternate only tints plots/selection.
+ CheckStatus severity = required == SemanticMeaning.Alternate ? CheckStatus.Warn : CheckStatus.Fail;
+ findings.Add(new Finding(
+ "Semantic coverage",
+ severity,
+ $"theme does not define {required}, which the ImGui mapper needs; those slots fall back to defaults"));
+ }
+ }
+
+ return [.. provided.Where(m => !MapperUsedMeanings.Contains(m)).OrderBy(m => m.ToString(), StringComparer.Ordinal)];
+ }
+
+ private static Color? ToColor(IReadOnlyDictionary imgui, ImGuiCol slot) =>
+ imgui.TryGetValue(slot, out Vector4 v) ? Color.FromSrgb(v.X, v.Y, v.Z, v.W) : null;
+
+ private static double HueDelta(double a, double b)
+ {
+ double d = Math.Abs(a - b) % 360.0;
+ return d > 180.0 ? 360.0 - d : d;
+ }
+
+ private static string Num(double value, string format = "0.0") =>
+ value.ToString(format, CultureInfo.InvariantCulture);
+}
diff --git a/ThemeProvider.Analysis/Program.cs b/ThemeProvider.Analysis/Program.cs
new file mode 100644
index 0000000..9e0534f
--- /dev/null
+++ b/ThemeProvider.Analysis/Program.cs
@@ -0,0 +1,82 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ThemeProvider.Analysis;
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+
+///
+/// Command-line entry point. Analyzes the registered themes, writes a markdown report, and returns a
+/// non-zero exit code when any theme fails a gate (so it doubles as a regression check).
+///
+internal static class Program
+{
+ private static int Main(string[] args)
+ {
+ string outputPath = "theme-analysis.md";
+ string? themeFilter = null;
+ bool strict = false;
+
+ for (int i = 0; i < args.Length; i++)
+ {
+ switch (args[i])
+ {
+ case "--output" or "-o" when i + 1 < args.Length:
+ outputPath = args[++i];
+ break;
+ case "--theme" or "-t" when i + 1 < args.Length:
+ themeFilter = args[++i];
+ break;
+ case "--strict":
+ strict = true;
+ break;
+ case "--help" or "-h":
+ PrintUsage();
+ return 0;
+ default:
+ Console.Error.WriteLine(Inv($"Unknown or incomplete argument: {args[i]}"));
+ PrintUsage();
+ return 2;
+ }
+ }
+
+ IReadOnlyList themes = themeFilter is null
+ ? ThemeRegistry.AllThemes
+ : [.. ThemeRegistry.AllThemes.Where(t => t.Name.Contains(themeFilter, StringComparison.OrdinalIgnoreCase))];
+
+ if (themes.Count == 0)
+ {
+ Console.Error.WriteLine(Inv($"No themes matched \"{themeFilter}\"."));
+ return 2;
+ }
+
+ List analyses = [.. themes.Select(PaletteAnalyzer.Analyze)];
+ string report = MarkdownReport.Build(analyses);
+ File.WriteAllText(outputPath, report);
+
+ int failed = analyses.Count(a => a.Status == CheckStatus.Fail);
+ int warned = analyses.Count(a => a.Status == CheckStatus.Warn);
+ int passed = analyses.Count - failed - warned;
+
+ Console.WriteLine(Inv($"Analyzed {analyses.Count} theme(s): {passed} pass, {warned} warn, {failed} fail."));
+ Console.WriteLine(Inv($"Report written to {Path.GetFullPath(outputPath)}"));
+
+ bool regressed = failed > 0 || (strict && warned > 0);
+ return regressed ? 1 : 0;
+ }
+
+ private static void PrintUsage()
+ {
+ Console.WriteLine("Usage: ThemeProvider.Analysis [--output ] [--theme ] [--strict]");
+ Console.WriteLine(" --output, -o Markdown report path (default: theme-analysis.md)");
+ Console.WriteLine(" --theme, -t Analyze only themes whose name contains this text");
+ Console.WriteLine(" --strict Exit non-zero on warnings as well as failures");
+ }
+
+ private static string Inv(FormattableString text) => text.ToString(CultureInfo.InvariantCulture);
+}
diff --git a/ThemeProvider.Analysis/ThemeProvider.Analysis.csproj b/ThemeProvider.Analysis/ThemeProvider.Analysis.csproj
new file mode 100644
index 0000000..b37b68b
--- /dev/null
+++ b/ThemeProvider.Analysis/ThemeProvider.Analysis.csproj
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+ net10.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ThemeProvider.sln b/ThemeProvider.sln
index 77cc420..78c1ad4 100644
--- a/ThemeProvider.sln
+++ b/ThemeProvider.sln
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThemeProvider.ImGui", "Them
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThemeProvider.Test", "ThemeProvider.Test\ThemeProvider.Test.csproj", "{202F4B12-82DA-45D3-94CB-75B8C10A0659}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThemeProvider.Analysis", "ThemeProvider.Analysis\ThemeProvider.Analysis.csproj", "{3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -69,6 +71,18 @@ Global
{202F4B12-82DA-45D3-94CB-75B8C10A0659}.Release|x64.Build.0 = Release|Any CPU
{202F4B12-82DA-45D3-94CB-75B8C10A0659}.Release|x86.ActiveCfg = Release|Any CPU
{202F4B12-82DA-45D3-94CB-75B8C10A0659}.Release|x86.Build.0 = Release|Any CPU
+ {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Debug|x64.Build.0 = Debug|Any CPU
+ {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Debug|x86.Build.0 = Debug|Any CPU
+ {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Release|x64.ActiveCfg = Release|Any CPU
+ {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Release|x64.Build.0 = Release|Any CPU
+ {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Release|x86.ActiveCfg = Release|Any CPU
+ {3C7E6BE5-9B6B-4F70-A0C7-7468AF03BB4F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE