A header only C++20 library. One seed colour gives a complete Material 3 style scheme, light or dark, with the tones, roles and contrast guarantees of the baseline scheme, computed in OKLCH as a light weight stand in for HCT. The seed colour and the light or dark preference come from a chain of sources: an environment override, the XDG desktop portal (GNOME 47+ and KDE Plasma 6), kdeglobals on KDE, gsettings on GNOME, GTK settings, and a fallback that is always appended and cannot be left out. Desktop specific sources are only asked on their own desktop, so a stale gsettings value on KDE never wins. Programs can put sources of their own in front. The tests run without a desktop through a fake Context. They cover colour conversions, tone accuracy, the contrast of every role pair over 300 random seeds in both modes, every parser against real portal output, the order of the sources per desktop, the fallback and external program timeouts. The test suite was checked by breaking the code 22 ways, all are caught. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
420 lines
25 KiB
C++
420 lines
25 KiB
C++
// Tests for libmonet. Plain asserts, no framework, no real desktop needed: every
|
|
// source is exercised through a fake Context.
|
|
#include <sys/stat.h>
|
|
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <map>
|
|
|
|
#include "monet/monet.h"
|
|
|
|
using namespace monet;
|
|
namespace fs = std::filesystem;
|
|
|
|
static int failures = 0;
|
|
#define CHECK(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); \
|
|
failures++; \
|
|
} \
|
|
} while (0)
|
|
#define CHECK_MSG(cond, ...) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
std::printf("FAIL line %d: %s ", __LINE__, #cond); \
|
|
std::printf(__VA_ARGS__); \
|
|
std::printf("\n"); \
|
|
failures++; \
|
|
} \
|
|
} while (0)
|
|
|
|
static bool same(Rgb a, Rgb b, int tolerance = 0) {
|
|
return std::abs(a.r - b.r) <= tolerance && std::abs(a.g - b.g) <= tolerance && std::abs(a.b - b.b) <= tolerance;
|
|
}
|
|
|
|
// --- a fake system -----------------------------------------------------------
|
|
|
|
struct Fake {
|
|
std::map<std::string, std::string> env;
|
|
std::map<std::string, std::string> programs; // argv[0] -> standard output
|
|
std::vector<std::string> called;
|
|
fs::path configHome;
|
|
|
|
Context context() {
|
|
Context c;
|
|
c.getenv = [this](const std::string &k) -> std::optional<std::string> {
|
|
auto it = env.find(k);
|
|
return it == env.end() ? std::nullopt : std::optional<std::string>(it->second);
|
|
};
|
|
c.run = [this](const std::vector<std::string> &argv, int) -> std::optional<std::string> {
|
|
called.push_back(argv[0]);
|
|
auto it = programs.find(argv[0]);
|
|
return it == programs.end() ? std::nullopt : std::optional<std::string>(it->second);
|
|
};
|
|
c.configHome = configHome;
|
|
c.home = configHome;
|
|
return c;
|
|
}
|
|
};
|
|
|
|
static const char *kBusctl =
|
|
"a{sa{sv}} 1 \"org.freedesktop.appearance\" 4 \"contrast\" u 0 \"color-scheme\" u 2 \"reduced-motion\" u 0 "
|
|
"\"accent-color\" (ddd) 0.239216 0.682353 0.913725\n";
|
|
static const char *kBusctlDark =
|
|
"a{sa{sv}} 1 \"org.freedesktop.appearance\" 2 \"color-scheme\" u 1 \"accent-color\" (ddd) 0.5 0.25 1\n";
|
|
static const char *kGdbus =
|
|
"({'org.freedesktop.appearance': {'contrast': <uint32 0>, 'color-scheme': <uint32 1>, 'accent-color': <(0.5, 0.25, 1.0)>}},)\n";
|
|
|
|
static void writeFile(const fs::path &p, const std::string &text) {
|
|
fs::create_directories(p.parent_path());
|
|
std::ofstream(p) << text;
|
|
}
|
|
|
|
int main() {
|
|
// --- colour spaces -------------------------------------------------------
|
|
{
|
|
const Rgb samples[] = {{0, 0, 0}, {255, 255, 255}, {255, 0, 0}, {0, 255, 0}, {0, 0, 255},
|
|
{61, 174, 233}, {255, 176, 201}, {128, 128, 128}, {1, 2, 3}, {250, 250, 5}};
|
|
for (Rgb c : samples) {
|
|
CHECK_MSG(same(toRgb(toOklch(c)), c, 1), "roundtrip of %s gave %s", hex(c).c_str(), hex(toRgb(toOklch(c))).c_str());
|
|
}
|
|
const Oklch white = toOklch({255, 255, 255});
|
|
CHECK(std::fabs(white.L - 1.0) < 0.002 && white.C < 0.002);
|
|
const Oklch red = toOklch({255, 0, 0});
|
|
CHECK(std::fabs(red.L - 0.628) < 0.003 && std::fabs(red.C - 0.2577) < 0.003 && std::fabs(red.h - 29.23) < 0.3);
|
|
CHECK(toOklch({0, 0, 0}).L < 1e-6);
|
|
CHECK(hex({0x3D, 0xAE, 0xE9}) == "#3DAEE9");
|
|
CHECK(std::fabs(toneOf({255, 255, 255}) - 100) < 0.01 && toneOf({0, 0, 0}) < 0.01);
|
|
CHECK(std::fabs(contrast({0, 0, 0}, {255, 255, 255}) - 21.0) < 0.01);
|
|
CHECK(std::fabs(contrast({90, 90, 90}, {90, 90, 90}) - 1.0) < 1e-9);
|
|
}
|
|
|
|
// --- tones ---------------------------------------------------------------
|
|
{
|
|
const double hues[] = {0, 30, 90, 150, 210, 270, 330};
|
|
const double chromas[] = {0.0, 0.05, 0.12, 0.25};
|
|
for (double h : hues) {
|
|
for (double c : chromas) {
|
|
double lastLum = -1;
|
|
for (double tone = 5; tone <= 95; tone += 5) {
|
|
const Rgb col = toneColor(h, c, tone);
|
|
CHECK_MSG(std::fabs(toneOf(col) - tone) < 0.9, "hue %.0f chroma %.2f tone %.0f gave %.2f", h, c, tone, toneOf(col));
|
|
const double lum = luminance(col);
|
|
CHECK_MSG(lum >= lastLum, "tones are not increasing at hue %.0f chroma %.2f tone %.0f", h, c, tone);
|
|
lastLum = lum;
|
|
}
|
|
}
|
|
}
|
|
CHECK(toneColor(200, 0.2, 0) == (Rgb{0, 0, 0}));
|
|
CHECK(toneColor(200, 0.2, 100) == (Rgb{255, 255, 255}));
|
|
CHECK(toneColor(200, 0.2, -5) == (Rgb{0, 0, 0}));
|
|
CHECK(toneColor(200, 0.2, 250) == (Rgb{255, 255, 255}));
|
|
// a grey stays grey
|
|
const Rgb g = toneColor(123, 0.0, 50);
|
|
CHECK(g.r == g.g && g.g == g.b);
|
|
// the chroma is kept where the gamut allows it and the hue stays the seed's
|
|
const Oklch mid = toOklch(toneColor(250, 0.10, 50));
|
|
CHECK(std::fabs(mid.h - 250) < 3 && mid.C > 0.08);
|
|
}
|
|
|
|
// --- schemes -------------------------------------------------------------
|
|
{
|
|
unsigned state = 12345;
|
|
auto next = [&] { state = state * 1664525u + 1013904223u; return static_cast<std::uint8_t>(state >> 24); };
|
|
std::vector<Rgb> seeds = {{61, 174, 233}, {255, 176, 201}, {0, 0, 0}, {255, 255, 255}, {128, 128, 128},
|
|
{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 0}, {0x67, 0x50, 0xA4}};
|
|
for (int i = 0; i < 300; i++) seeds.push_back({next(), next(), next()});
|
|
for (Rgb seed : seeds) {
|
|
for (Mode mode : {Mode::Light, Mode::Dark}) {
|
|
const Scheme s = generate(seed, mode);
|
|
const char *m = mode == Mode::Light ? "light" : "dark";
|
|
auto pair = [&](const char *name, Rgb bg, Rgb fg, double minimum) {
|
|
CHECK_MSG(contrast(bg, fg) >= minimum, "%s %s seed %s: contrast %.2f < %.1f", m, name, hex(seed).c_str(), contrast(bg, fg), minimum);
|
|
};
|
|
pair("primary", s.primary, s.onPrimary, 4.5);
|
|
pair("primaryContainer", s.primaryContainer, s.onPrimaryContainer, 7.0);
|
|
pair("secondary", s.secondary, s.onSecondary, 4.5);
|
|
pair("secondaryContainer", s.secondaryContainer, s.onSecondaryContainer, 7.0);
|
|
pair("tertiary", s.tertiary, s.onTertiary, 4.5);
|
|
pair("tertiaryContainer", s.tertiaryContainer, s.onTertiaryContainer, 7.0);
|
|
pair("error", s.error, s.onError, 4.5);
|
|
pair("errorContainer", s.errorContainer, s.onErrorContainer, 7.0);
|
|
pair("surface", s.surface, s.onSurface, 7.0);
|
|
pair("background", s.background, s.onBackground, 7.0);
|
|
pair("surfaceContainerHighest", s.surfaceContainerHighest, s.onSurface, 7.0);
|
|
pair("surfaceContainerLowest", s.surfaceContainerLowest, s.onSurface, 7.0);
|
|
pair("surfaceVariant", s.surfaceVariant, s.onSurfaceVariant, 4.5);
|
|
pair("inverse", s.inverseSurface, s.inverseOnSurface, 7.0);
|
|
pair("outline", s.surface, s.outline, 3.0);
|
|
pair("inversePrimary", s.inverseSurface, s.inversePrimary, 3.0);
|
|
// the surface ladder is ordered, towards the light in light mode and towards the dark in dark mode
|
|
const double l[] = {luminance(s.surfaceContainerLowest), luminance(s.surfaceContainerLow), luminance(s.surfaceContainer),
|
|
luminance(s.surfaceContainerHigh), luminance(s.surfaceContainerHighest)};
|
|
for (int k = 0; k < 4; k++) {
|
|
CHECK_MSG(mode == Mode::Light ? l[k] > l[k + 1] : l[k] < l[k + 1], "%s surface ladder step %d seed %s", m, k, hex(seed).c_str());
|
|
}
|
|
CHECK(s.seed == seed && s.mode == mode && s.dark() == (mode == Mode::Dark));
|
|
// same input, same output
|
|
const Scheme again = generate(seed, mode);
|
|
CHECK(same(again.primary, s.primary) && same(again.surface, s.surface) && same(again.outline, s.outline));
|
|
}
|
|
CHECK(luminance(generate(seed, Mode::Dark).surface) < luminance(generate(seed, Mode::Light).surface));
|
|
}
|
|
// the accent keeps the seed's hue
|
|
for (Rgb seed : {Rgb{61, 174, 233}, Rgb{255, 176, 201}, Rgb{0x67, 0x50, 0xA4}, Rgb{200, 60, 20}}) {
|
|
const Oklch want = toOklch(seed);
|
|
for (Mode mode : {Mode::Light, Mode::Dark}) {
|
|
const Oklch got = toOklch(generate(seed, mode).primary);
|
|
CHECK_MSG(got.C < 0.04 || std::fabs(std::remainder(got.h - want.h, 360.0)) < 4.0, "hue of %s: seed %.1f primary %.1f", hex(seed).c_str(), want.h, got.h);
|
|
}
|
|
}
|
|
// the tertiary accent sits 60 degrees round the hue circle from the primary, the secondary does not
|
|
for (Rgb seed : {Rgb{61, 174, 233}, Rgb{255, 176, 201}, Rgb{0x67, 0x50, 0xA4}, Rgb{200, 60, 20}, Rgb{30, 160, 60}}) {
|
|
for (Mode mode : {Mode::Light, Mode::Dark}) {
|
|
const Scheme s = generate(seed, mode);
|
|
const Oklch p = toOklch(s.primary), t = toOklch(s.tertiary), c = toOklch(s.secondary);
|
|
const double shift = std::remainder(t.h - p.h, 360.0);
|
|
CHECK_MSG(std::fabs(shift - 60.0) < 8.0, "tertiary of %s is %.1f degrees from the primary", hex(seed).c_str(), shift);
|
|
CHECK_MSG(c.C < 0.03 || std::fabs(std::remainder(c.h - p.h, 360.0)) < 8.0, "secondary of %s left the hue", hex(seed).c_str());
|
|
// and it is a real accent, not a neutral
|
|
CHECK(t.C > 0.03 && c.C > 0.015);
|
|
}
|
|
}
|
|
// the neutrals are almost grey, the error accent is red whatever the seed
|
|
for (Rgb seed : {Rgb{61, 174, 233}, Rgb{30, 160, 60}, Rgb{255, 176, 201}}) {
|
|
const Scheme s = generate(seed, Mode::Light);
|
|
CHECK(toOklch(s.surface).C < 0.03 && toOklch(s.surfaceVariant).C < 0.04);
|
|
const Oklch e = toOklch(s.error);
|
|
CHECK_MSG(std::fabs(std::remainder(e.h - 29.0, 360.0)) < 12.0 && e.C > 0.12, "error accent of %s: hue %.1f chroma %.3f", hex(seed).c_str(), e.h, e.C);
|
|
}
|
|
// a grey seed gives a grey scheme, a colourful one does not
|
|
for (Rgb grey : {Rgb{128, 128, 128}, Rgb{0, 0, 0}, Rgb{255, 255, 255}, Rgb{40, 41, 40}}) {
|
|
const Scheme s = generate(grey, Mode::Light);
|
|
CHECK_MSG(toOklch(s.primary).C < 0.03, "grey seed %s gave chroma %.3f", hex(grey).c_str(), toOklch(s.primary).C);
|
|
}
|
|
CHECK(toOklch(generate({61, 174, 233}, Mode::Light).primary).C > 0.09);
|
|
}
|
|
|
|
// --- parsers -------------------------------------------------------------
|
|
{
|
|
CHECK(parse::hexColor("#3DAEE9") == std::optional<Rgb>(Rgb{0x3D, 0xAE, 0xE9}));
|
|
CHECK(parse::hexColor("3daee9") == std::optional<Rgb>(Rgb{0x3D, 0xAE, 0xE9}));
|
|
CHECK(parse::hexColor(" #fff ") == std::optional<Rgb>(Rgb{255, 255, 255}));
|
|
CHECK(!parse::hexColor("") && !parse::hexColor("#") && !parse::hexColor("#12") && !parse::hexColor("zzzzzz"));
|
|
CHECK(!parse::hexColor("#3DAEE9FF") && !parse::hexColor("banana"));
|
|
|
|
CHECK(parse::kdeRgb("61,174,233") == std::optional<Rgb>(Rgb{61, 174, 233}));
|
|
CHECK(parse::kdeRgb(" 61, 174, 233 ") == std::optional<Rgb>(Rgb{61, 174, 233}));
|
|
CHECK(!parse::kdeRgb("300,0,0") && !parse::kdeRgb("1,2") && !parse::kdeRgb("1,2,3,4") && !parse::kdeRgb("a,b,c"));
|
|
CHECK(!parse::kdeRgb("") && !parse::kdeRgb("-1,0,0") && !parse::kdeRgb("1 2 3"));
|
|
|
|
CHECK(parse::gnomeScheme("'prefer-dark'\n") == std::optional<Mode>(Mode::Dark));
|
|
CHECK(parse::gnomeScheme("'prefer-light'") == std::optional<Mode>(Mode::Light));
|
|
CHECK(!parse::gnomeScheme("'default'") && !parse::gnomeScheme("") && !parse::gnomeScheme("garbage"));
|
|
CHECK(parse::gnomeAccent("'pink'\n") == std::optional<Rgb>(Rgb{0xd5, 0x61, 0x99}));
|
|
CHECK(parse::gnomeAccent("'blue'") == std::optional<Rgb>(Rgb{0x35, 0x84, 0xe4}));
|
|
CHECK(parse::gnomeAccent("'Slate'") == std::optional<Rgb>(Rgb{0x6f, 0x83, 0x96}));
|
|
CHECK(!parse::gnomeAccent("'magenta'") && !parse::gnomeAccent(""));
|
|
|
|
// the real output of this machine
|
|
CHECK(parse::portalScheme(kBusctl) == std::optional<Mode>(Mode::Light));
|
|
CHECK(parse::portalAccent(kBusctl) == std::optional<Rgb>(Rgb{61, 174, 233}));
|
|
CHECK(parse::portalScheme(kBusctlDark) == std::optional<Mode>(Mode::Dark));
|
|
CHECK(parse::portalAccent(kBusctlDark) == std::optional<Rgb>(Rgb{128, 64, 255}));
|
|
// gdbus prints it differently
|
|
CHECK(parse::portalScheme(kGdbus) == std::optional<Mode>(Mode::Dark));
|
|
CHECK(parse::portalAccent(kGdbus) == std::optional<Rgb>(Rgb{128, 64, 255}));
|
|
// no preference, no accent, missing keys, a different order, nonsense
|
|
CHECK(!parse::portalScheme("\"color-scheme\" u 0 \"x\" u 1"));
|
|
CHECK(!parse::portalAccent("\"accent-color\" (ddd) -1 -1 -1"));
|
|
CHECK(!parse::portalAccent("\"accent-color\" (ddd) 2 0.5 0.5"));
|
|
CHECK(!parse::portalAccent("\"accent-color\" (ddd) 0.5 0.5"));
|
|
CHECK(!parse::portalScheme("") && !parse::portalAccent("") && !parse::portalScheme("nothing here"));
|
|
CHECK(parse::portalAccent("\"accent-color\" (ddd) 0.1 0.2 0.3 \"color-scheme\" u 1") == std::optional<Rgb>(Rgb{26, 51, 77}));
|
|
CHECK(parse::portalScheme("\"accent-color\" (ddd) 0.1 0.2 0.3 \"color-scheme\" u 1") == std::optional<Mode>(Mode::Dark));
|
|
// the 32 of uint32 and the 3 of (ddd) are not values
|
|
CHECK(parse::portalScheme("'color-scheme': <uint32 2>") == std::optional<Mode>(Mode::Light));
|
|
}
|
|
{
|
|
const std::string light =
|
|
"[General]\nAccentColor=61,174,233\nColorScheme=BreezeLight\n[Colors:Window]\nBackgroundNormal=239,240,241\n";
|
|
Appearance a = parse::kdeGlobals(light);
|
|
CHECK(a.accent == std::optional<Rgb>(Rgb{61, 174, 233}) && a.mode == std::optional<Mode>(Mode::Light));
|
|
// no accent set: the selection colour of the scheme
|
|
a = parse::kdeGlobals("[Colors:Selection]\nBackgroundNormal=100,50,200\n[Colors:Window]\nBackgroundNormal=35,38,41\n");
|
|
CHECK(a.accent == std::optional<Rgb>(Rgb{100, 50, 200}) && a.mode == std::optional<Mode>(Mode::Dark));
|
|
// a broken accent falls back to the selection colour
|
|
a = parse::kdeGlobals("[General]\nAccentColor=abc\n[Colors:Selection]\nBackgroundNormal=1,2,3\n");
|
|
CHECK(a.accent == std::optional<Rgb>(Rgb{1, 2, 3}));
|
|
// the scheme name when there are no colours
|
|
CHECK(parse::kdeGlobals("[General]\nColorScheme=BreezeDark\n").mode == std::optional<Mode>(Mode::Dark));
|
|
CHECK(parse::kdeGlobals("[General]\nColorScheme=Foo\n").mode == std::optional<Mode>(Mode::Light));
|
|
// comments, blank lines and garbage are ignored, keys of other sections are not mixed up
|
|
a = parse::kdeGlobals("# comment\n\nnot a line\n[Other]\nAccentColor=9,9,9\n[Colors:View]\nBackgroundNormal=0,0,0\n");
|
|
CHECK(!a.accent && !a.mode);
|
|
CHECK(!parse::kdeGlobals("").accent && !parse::kdeGlobals("").mode);
|
|
CHECK(parse::gtkSettings("[Settings]\ngtk-application-prefer-dark-theme=1\n") == std::optional<Mode>(Mode::Dark));
|
|
CHECK(parse::gtkSettings("gtk-theme-name=Orchis-Dark\n") == std::optional<Mode>(Mode::Dark));
|
|
CHECK(parse::gtkSettings("gtk-theme-name=Adwaita\n") == std::optional<Mode>(Mode::Light));
|
|
CHECK(!parse::gtkSettings("gtk-font-name=Sans 10\n"));
|
|
}
|
|
|
|
// --- the sources, through a fake system ------------------------------------
|
|
{
|
|
char tmpl[] = "/tmp/monet_test.XXXXXX";
|
|
const fs::path dir = mkdtemp(tmpl);
|
|
const std::string kdeFile =
|
|
"[General]\nAccentColor=10,200,30\n[Colors:Window]\nBackgroundNormal=30,30,30\n";
|
|
|
|
{ // KDE: the portal answers, it is used and kdeglobals is not needed
|
|
Fake f; f.configHome = dir / "a"; f.env["XDG_CURRENT_DESKTOP"] = "KDE";
|
|
f.programs["busctl"] = kBusctl;
|
|
writeFile(f.configHome / "kdeglobals", kdeFile);
|
|
Context c = f.context();
|
|
const Appearance a = detect(c, defaultSources(c));
|
|
CHECK(a.accent == std::optional<Rgb>(Rgb{61, 174, 233}) && a.accentSource == "portal");
|
|
CHECK(a.mode == std::optional<Mode>(Mode::Light) && a.modeSource == "portal");
|
|
}
|
|
{ // KDE without a portal: the file
|
|
Fake f; f.configHome = dir / "b"; f.env["XDG_CURRENT_DESKTOP"] = "KDE";
|
|
writeFile(f.configHome / "kdeglobals", kdeFile);
|
|
Context c = f.context();
|
|
const Appearance a = detect(c, defaultSources(c));
|
|
CHECK(a.accent == std::optional<Rgb>(Rgb{10, 200, 30}) && a.accentSource == "kde");
|
|
CHECK(a.mode == std::optional<Mode>(Mode::Dark) && a.modeSource == "kde");
|
|
}
|
|
{ // GNOME without a portal: gsettings
|
|
Fake f; f.configHome = dir / "c"; f.env["XDG_CURRENT_DESKTOP"] = "ubuntu:GNOME";
|
|
f.programs["gsettings"] = "'prefer-dark'\n";
|
|
Context c = f.context();
|
|
const Appearance a = detect(c, defaultSources(c));
|
|
CHECK(a.modeSource == "gnome" && a.mode == std::optional<Mode>(Mode::Dark));
|
|
}
|
|
{ // a leftover gsettings value on KDE must not beat the desktop's own answer
|
|
Fake f; f.configHome = dir / "d"; f.env["XDG_CURRENT_DESKTOP"] = "KDE";
|
|
f.programs["gsettings"] = "'pink'\n";
|
|
writeFile(f.configHome / "kdeglobals", kdeFile);
|
|
Context c = f.context();
|
|
const Appearance a = detect(c, defaultSources(c));
|
|
CHECK(a.accent == std::optional<Rgb>(Rgb{10, 200, 30}) && a.accentSource == "kde");
|
|
CHECK(std::find(f.called.begin(), f.called.end(), "gsettings") == f.called.end());
|
|
}
|
|
{ // the override wins over everything
|
|
Fake f; f.configHome = dir / "e"; f.env["XDG_CURRENT_DESKTOP"] = "KDE";
|
|
f.env["MONET_SEED"] = "#112233"; f.env["MONET_MODE"] = "Light";
|
|
f.programs["busctl"] = kBusctlDark;
|
|
Context c = f.context();
|
|
const Appearance a = detect(c, defaultSources(c));
|
|
CHECK(a.accent == std::optional<Rgb>(Rgb{0x11, 0x22, 0x33}) && a.accentSource == "override");
|
|
CHECK(a.mode == std::optional<Mode>(Mode::Light) && a.modeSource == "override");
|
|
}
|
|
{ // a broken override is ignored, not fatal
|
|
Fake f; f.configHome = dir / "f"; f.env["MONET_SEED"] = "banana"; f.env["MONET_MODE"] = "purple";
|
|
f.programs["busctl"] = kBusctlDark;
|
|
Context c = f.context();
|
|
const Appearance a = detect(c, defaultSources(c));
|
|
CHECK(a.accentSource == "portal" && a.modeSource == "portal");
|
|
}
|
|
{ // each value comes from the first source that has it
|
|
Fake f; f.configHome = dir / "g"; f.env["XDG_CURRENT_DESKTOP"] = "KDE";
|
|
f.programs["busctl"] = "\"color-scheme\" u 1";
|
|
writeFile(f.configHome / "kdeglobals", "[General]\nAccentColor=1,2,3\n[Colors:Window]\nBackgroundNormal=255,255,255\n");
|
|
Context c = f.context();
|
|
const Appearance a = detect(c, defaultSources(c));
|
|
CHECK(a.mode == std::optional<Mode>(Mode::Dark) && a.modeSource == "portal");
|
|
CHECK(a.accent == std::optional<Rgb>(Rgb{1, 2, 3}) && a.accentSource == "kde");
|
|
}
|
|
{ // the fallback: nothing is available at all
|
|
Fake f; f.configHome = dir / "h";
|
|
Context c = f.context();
|
|
const Appearance a = detect(c, defaultSources(c));
|
|
CHECK(a.accent == std::optional<Rgb>(kDefaultSeed) && a.accentSource == "fallback");
|
|
CHECK(a.mode == std::optional<Mode>(kDefaultMode) && a.modeSource == "fallback");
|
|
}
|
|
{ // and it is there even when the list of sources is empty or every source fails
|
|
Fake f; f.configHome = dir / "i";
|
|
Context c = f.context();
|
|
const Appearance a = detect(c, {});
|
|
CHECK(a.accent && a.mode && a.accentSource == "fallback");
|
|
Source broken{"broken", [](const Context &) { return Appearance{}; }};
|
|
const Appearance b = detect(c, {broken, broken});
|
|
CHECK(b.accent == std::optional<Rgb>(kDefaultSeed) && b.modeSource == "fallback");
|
|
const Scheme s = fromDesktop(c);
|
|
CHECK(s.seed == kDefaultSeed && s.mode == kDefaultMode);
|
|
}
|
|
{ // a source of the program's own is asked first
|
|
Fake f; f.configHome = dir / "j"; f.programs["busctl"] = kBusctl;
|
|
Context c = f.context();
|
|
Source mine{"mine", [](const Context &) { Appearance a; a.accent = Rgb{9, 8, 7}; return a; }};
|
|
const Scheme s = fromDesktop(c, {mine});
|
|
CHECK(s.seed == (Rgb{9, 8, 7}) && s.mode == Mode::Light);
|
|
}
|
|
{ // an unknown desktop does not consult the desktop specific sources
|
|
Fake f; f.configHome = dir / "k";
|
|
f.programs["gsettings"] = "'prefer-dark'";
|
|
Context c = f.context();
|
|
detect(c, defaultSources(c));
|
|
CHECK(std::find(f.called.begin(), f.called.end(), "gsettings") == f.called.end());
|
|
}
|
|
{ // the list of sources per desktop
|
|
auto names = [&](const char *desktop) {
|
|
Fake f; f.configHome = dir / "n";
|
|
if (desktop) f.env["XDG_CURRENT_DESKTOP"] = desktop;
|
|
std::string out;
|
|
for (const auto &s : defaultSources(f.context())) out += s.name + " ";
|
|
return out;
|
|
};
|
|
CHECK(names("KDE") == "override portal kde gtk ");
|
|
CHECK(names("GNOME") == "override portal gnome gtk ");
|
|
CHECK(names("X-Cinnamon") == "override portal gnome gtk ");
|
|
CHECK(names("sway") == "override portal gtk ");
|
|
CHECK(names(nullptr) == "override portal gtk ");
|
|
}
|
|
{ // gtk settings give the mode on a desktop with nothing better
|
|
Fake f; f.configHome = dir / "o";
|
|
writeFile(f.configHome / "gtk-3.0/settings.ini", "[Settings]\ngtk-application-prefer-dark-theme=1\n");
|
|
Context c = f.context();
|
|
const Appearance a = detect(c, defaultSources(c));
|
|
CHECK(a.mode == std::optional<Mode>(Mode::Dark) && a.modeSource == "gtk");
|
|
CHECK(a.accentSource == "fallback");
|
|
}
|
|
{ // the portal is read through gdbus when there is no busctl
|
|
Fake f; f.configHome = dir / "p"; f.programs["gdbus"] = kGdbus;
|
|
Context c = f.context();
|
|
const Appearance a = detect(c, defaultSources(c));
|
|
CHECK(a.accent == std::optional<Rgb>(Rgb{128, 64, 255}) && a.accentSource == "portal" && a.mode == std::optional<Mode>(Mode::Dark));
|
|
}
|
|
fs::remove_all(dir);
|
|
}
|
|
|
|
// --- running real programs -------------------------------------------------
|
|
{
|
|
auto ms = [](auto from) { return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - from).count(); };
|
|
CHECK(detail::runProgram({"echo", "hi"}, 2000) == std::optional<std::string>("hi\n"));
|
|
CHECK(!detail::runProgram({"definitely-not-a-program-xyz"}, 2000));
|
|
CHECK(!detail::runProgram({"false"}, 2000));
|
|
CHECK(!detail::runProgram({}, 2000));
|
|
CHECK(detail::runProgram({"cat"}, 2000) == std::optional<std::string>("")); // stdin is closed
|
|
auto t = std::chrono::steady_clock::now();
|
|
CHECK(!detail::runProgram({"sleep", "5"}, 150));
|
|
CHECK_MSG(ms(t) < 1500, "timeout took %lld ms", static_cast<long long>(ms(t)));
|
|
t = std::chrono::steady_clock::now();
|
|
CHECK(!detail::runProgram({"head", "-c", "3000000", "/dev/zero"}, 5000)); // far more than an answer
|
|
CHECK_MSG(ms(t) < 3000, "oversized output took %lld ms", static_cast<long long>(ms(t)));
|
|
// the real system: the call must come back, with or without a desktop
|
|
t = std::chrono::steady_clock::now();
|
|
const Appearance real = detect(Context::system(), defaultSources(Context::system()));
|
|
CHECK(real.accent && real.mode && !real.accentSource.empty());
|
|
CHECK_MSG(ms(t) < 2500, "detecting on this machine took %lld ms", static_cast<long long>(ms(t)));
|
|
std::printf("this machine: accent %s from %s, %s from %s, %lld ms\n", hex(*real.accent).c_str(), real.accentSource.c_str(),
|
|
*real.mode == Mode::Dark ? "dark" : "light", real.modeSource.c_str(), static_cast<long long>(ms(t)));
|
|
}
|
|
|
|
std::printf(failures ? "%d check(s) failed\n" : "libmonet: all checks passed\n", failures);
|
|
return failures ? 1 : 0;
|
|
}
|