Proof Portal

Project overview

Omarchy

ProbeLabs65 findings · 95 requirements

A proof layer — requirements, tests and verified fixes — for two of Omarchy's subsystems: the application menu (launcher scripts, QML model, JSONC config, search and selection) and the lock screen (lock scripts, QML, PAM authentication). Scope is deliberately limited to those components of omacom/omarchy; the rest of the distribution is not covered.

Inbox
ReportR-888GitHub issuePublicDecided· Out of scope

Themed dimText falls below WCAG AA contrast on 19 of 22 first-party themes

Reported by slhuckstead · filed Sep 6, 2026 · open at source

Decided: Out of scope — decided by agents · verified 4 days ago.

At a glance

1 · What is claimed?
Only the report text below — no validator has restated it as checkable claims yet.
2 · What does it affect?
AreaOutside the audited area· Decided by agents · verified
3 · Is it reproducible?
No validation has been run yet.
4 · What remains unclear?
  • Not validated yet: no claims, no reproducer.
5 · What happens next?
Decided by agents and verified by a second, independent agent — see the decision.

Original report

Themed dimText falls below WCAG AA contrast on 19 of 22 first-party themesrevision 1 · captured 2026-09-28 21:23 UTC via poll

System details

Omarchy 4.0.2, Arch, Hyprland. Not hardware-specific — this lives in
default/themed/*.tpl and reproduces from the shipped theme files alone.
Confirmed present at quattro HEAD.

What's wrong?

default/themed/pi.json.tpl derives dimmed text with a fixed blend toward the
background:

34:    "mutedText": "{{ mix foreground background 34% }}",
35:    "dimText":   "{{ mix foreground background 52% }}"

dimText isn't decoration — the same file maps it to text people read:

46:    "dim": "dimText",
48:    "thinkingText": "dimText",
73:    "syntaxComment": "dimText",

A fixed percentage assumes every theme has the same foreground-to-background
headroom, and they don't. Mixing toward the background also drops luminance
non-linearly, so the same 52% costs far more contrast on a low-headroom theme
than a high-headroom one.

Measured across the 22 themes in themes/ that ship a colors.toml,
19 of 22 land below the WCAG AA 4.5:1 minimum for normal text, and 6 fall
below even the 3:1 large-text floor:

themedimText @ 52%@ 25%
rose-pine2.17:13.70:1
catppuccin-latte2.21:13.85:1
tokyo-night2.89:15.15:1
everforest2.93:14.91:1
lupine2.98:16.92:1
gruvbox3.03:15.29:1
miasma3.14:15.59:1
matte-black3.21:16.10:1
osaka-jade3.22:15.96:1
flexoki-light3.36:18.59:1
nord3.41:16.00:1
solitude3.52:16.93:1
kanagawa3.65:16.92:1
ristretto3.66:16.83:1
catppuccin3.66:16.95:1
white3.69:110.37:1
ethereal3.80:17.87:1
lumon3.86:17.41:1
retro-823.92:17.90:1
hackerman4.61:19.91:1
vantablack4.89:111.42:1
last-horizon4.89:110.77:1

The same pattern appears in three other text-bearing tokens:

templatetokenmixbelow 4.5:1worst
pi.json.tpl:34mutedText → mdQuote, syntaxPunctuation34%5/223.08:1
claude.json.tpl:9inactive40%10/222.72:1
shell.toml.tpl:216placeholder34%5/223.08:1

Steps to reproduce

Reads only the shipped theme files; changes nothing:

python3 - <<'PY'
import re, glob, os
def lum(h):
    h = h.lstrip("#")[:6]
    c = [int(h[i:i+2], 16)/255 for i in (0, 2, 4)]
    c = [x/12.92 if x <= 0.04045 else ((x+0.055)/1.055)**2.4 for x in c]
    return 0.2126*c[0] + 0.7152*c[1] + 0.0722*c[2]
def ratio(a, b):
    la, lb = lum(a), lum(b)
    return (max(la, lb) + 0.05) / (min(la, lb) + 0.05)
def mix(a, b, amt):
    a, b = a.lstrip("#"), b.lstrip("#")
    return "#" + "".join(f"{int(int(a[i:i+2],16)*(1-amt)+int(b[i:i+2],16)*amt+0.5):02x}"
                         for i in (0, 2, 4))
bad = 0
for d in sorted(glob.glob("/usr/share/omarchy/themes/*")):
    f = os.path.join(d, "colors.toml")
    if not os.path.exists(f):
        continue
    t = open(f).read()
    m1 = re.search(r'^foreground\s*=\s*"(#\w{6})"', t, re.M)
    m2 = re.search(r'^background\s*=\s*"(#\w{6})"', t, re.M)
    if not (m1 and m2):
        continue
    fg, bg = m1.group(1), m2.group(1)
    r = ratio(mix(fg, bg, 0.52), bg)
    bad += r < 4.5
    print(f"{os.path.basename(d):20} dimText {mix(fg,bg,0.52)} = {r:5.2f}:1"
          f"{'  BELOW AA' if r < 4.5 else ''}")
print(f"\n{bad} themes below 4.5:1")
PY

Suggested fix

Lowering the constant helps but does not solve it. At 25%, dimText clears
4.5:1 on 20 of 22 — rose-pine (3.70:1) and catppuccin-latte (3.85:1) still
fail, because their own undimmed foreground is only 6.66:1 and 7.06:1, so any
meaningful dimming drops under. Going low enough to save those two (~15%) makes
dimText nearly indistinguishable from normal text, which defeats the token.

The robust fix is to solve for a contrast floor instead of a fixed blend:
mix toward the background until the result would drop below a target ratio,
then stop. Something like {{ dim foreground background 52% min 4.5 }}. That
keeps dim genuinely dim on themes with headroom, and stops it becoming
unreadable on themes without — without either constraining theme authors'
palettes or hand-tuning a constant per theme.

Happy to send a PR for whichever direction you'd prefer.

View raw text
### System details

Omarchy 4.0.2, Arch, Hyprland. Not hardware-specific — this lives in
`default/themed/*.tpl` and reproduces from the shipped theme files alone.
Confirmed present at `quattro` HEAD.

### What's wrong?

`default/themed/pi.json.tpl` derives dimmed text with a fixed blend toward the
background:

```
34:    "mutedText": "{{ mix foreground background 34% }}",
35:    "dimText":   "{{ mix foreground background 52% }}"
```

`dimText` isn't decoration — the same file maps it to text people read:

```
46:    "dim": "dimText",
48:    "thinkingText": "dimText",
73:    "syntaxComment": "dimText",
```

A fixed percentage assumes every theme has the same foreground-to-background
headroom, and they don't. Mixing toward the background also drops luminance
non-linearly, so the same 52% costs far more contrast on a low-headroom theme
than a high-headroom one.

Measured across the 22 themes in `themes/` that ship a `colors.toml`,
**19 of 22 land below the WCAG AA 4.5:1 minimum for normal text**, and 6 fall
below even the 3:1 large-text floor:

| theme | `dimText` @ 52% | @ 25% |
|---|---|---|
| rose-pine | 2.17:1 | 3.70:1 |
| catppuccin-latte | 2.21:1 | 3.85:1 |
| tokyo-night | 2.89:1 | 5.15:1 |
| everforest | 2.93:1 | 4.91:1 |
| lupine | 2.98:1 | 6.92:1 |
| gruvbox | 3.03:1 | 5.29:1 |
| miasma | 3.14:1 | 5.59:1 |
| matte-black | 3.21:1 | 6.10:1 |
| osaka-jade | 3.22:1 | 5.96:1 |
| flexoki-light | 3.36:1 | 8.59:1 |
| nord | 3.41:1 | 6.00:1 |
| solitude | 3.52:1 | 6.93:1 |
| kanagawa | 3.65:1 | 6.92:1 |
| ristretto | 3.66:1 | 6.83:1 |
| catppuccin | 3.66:1 | 6.95:1 |
| white | 3.69:1 | 10.37:1 |
| ethereal | 3.80:1 | 7.87:1 |
| lumon | 3.86:1 | 7.41:1 |
| retro-82 | 3.92:1 | 7.90:1 |
| hackerman | 4.61:1 | 9.91:1 |
| vantablack | 4.89:1 | 11.42:1 |
| last-horizon | 4.89:1 | 10.77:1 |

The same pattern appears in three other text-bearing tokens:

| template | token | mix | below 4.5:1 | worst |
|---|---|---|---|---|
| `pi.json.tpl:34` | `mutedText` → `mdQuote`, `syntaxPunctuation` | 34% | 5/22 | 3.08:1 |
| `claude.json.tpl:9` | `inactive` | 40% | 10/22 | 2.72:1 |
| `shell.toml.tpl:216` | `placeholder` | 34% | 5/22 | 3.08:1 |

### Steps to reproduce

Reads only the shipped theme files; changes nothing:

```bash
python3 - <<'PY'
import re, glob, os
def lum(h):
    h = h.lstrip("#")[:6]
    c = [int(h[i:i+2], 16)/255 for i in (0, 2, 4)]
    c = [x/12.92 if x <= 0.04045 else ((x+0.055)/1.055)**2.4 for x in c]
    return 0.2126*c[0] + 0.7152*c[1] + 0.0722*c[2]
def ratio(a, b):
    la, lb = lum(a), lum(b)
    return (max(la, lb) + 0.05) / (min(la, lb) + 0.05)
def mix(a, b, amt):
    a, b = a.lstrip("#"), b.lstrip("#")
    return "#" + "".join(f"{int(int(a[i:i+2],16)*(1-amt)+int(b[i:i+2],16)*amt+0.5):02x}"
                         for i in (0, 2, 4))
bad = 0
for d in sorted(glob.glob("/usr/share/omarchy/themes/*")):
    f = os.path.join(d, "colors.toml")
    if not os.path.exists(f):
        continue
    t = open(f).read()
    m1 = re.search(r'^foreground\s*=\s*"(#\w{6})"', t, re.M)
    m2 = re.search(r'^background\s*=\s*"(#\w{6})"', t, re.M)
    if not (m1 and m2):
        continue
    fg, bg = m1.group(1), m2.group(1)
    r = ratio(mix(fg, bg, 0.52), bg)
    bad += r < 4.5
    print(f"{os.path.basename(d):20} dimText {mix(fg,bg,0.52)} = {r:5.2f}:1"
          f"{'  BELOW AA' if r < 4.5 else ''}")
print(f"\n{bad} themes below 4.5:1")
PY
```

### Suggested fix

Lowering the constant helps but does not solve it. At **25%**, `dimText` clears
4.5:1 on 20 of 22 — `rose-pine` (3.70:1) and `catppuccin-latte` (3.85:1) still
fail, because their own *undimmed* foreground is only 6.66:1 and 7.06:1, so any
meaningful dimming drops under. Going low enough to save those two (~15%) makes
`dimText` nearly indistinguishable from normal text, which defeats the token.

The robust fix is to solve for a **contrast floor** instead of a fixed blend:
mix toward the background until the result would drop below a target ratio,
then stop. Something like `{{ dim foreground background 52% min 4.5 }}`. That
keeps dim genuinely dim on themes with headroom, and stops it becoming
unreadable on themes without — without either constraining theme authors'
palettes or hand-tuning a constant per theme.

Happy to send a PR for whichever direction you'd prefer.

Validation

Not checked yet. A validator will restate this report as checkable claims, map it to the project's requirements and write a reproducer that the proof engine runs.

Linked

Not linked to any known issue, other report or change yet.

Decision

Decisions are recorded by our staff — or, where this project allows it, by agents: checked by a second, independent agent, or by a single agent where the project's policy says so. Any decision reached so far is shown below.

  • Out of scopeDecided by agents · verified · 2026-10-09 11:01 UTC