Proof Portal
Omarchy
ProbeLabs65 findings · 95 requirementsA 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.
Themed dimText falls below WCAG AA contrast on 19 of 22 first-party themes
Reported by slhuckstead · filed Sep 6, 2026 · open at source
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
System details
Omarchy 4.0.2, Arch, Hyprland. Not hardware-specific — this lives indefault/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:
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")
PYSuggested 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%) makesdimText 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