You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
stylix.polarity = "either" is the default, yet no layer of Stylix
resolves it to a concrete "light" or "dark". Each consuming module
is left to decide on its own: some treat "either" as "dark", some
emit invalid output, some pick a variant arbitrarily. Users on default
settings see silently broken or inverted rendering whenever the resolved
palette is light, whether the generator chose a light scheme from the
wallpaper or the user supplied a light base16Scheme manually.
This issue is a tracker. Fixes are landing per-module ad hoc
(#2192, #2288); a wider direction is gestured at across #892 and #2289
but no consolidated approach exists.
Affected modules
Two distinct failure mechanisms share a single root cause.
Mechanism A: literal-token interpolation. The module embeds polarity directly into output where only "light" or "dark" is
valid. "either" produces a token nothing matches.
obsidian (modules/obsidian/hm.nix:40, 66)
Symptom: emits the invalid CSS class .theme-either; theme silently
fails to apply.
Mechanism B: branch-selection desync. The module supplies both
polarity branches and lets the consumer pick at runtime. The consumer's
runtime signal (terminal background detection, system theme) can
disagree with Stylix's actually-resolved polarity, selecting the wrong
row.
opencode (modules/opencode/hm.nix)
Symptom: when opencode's runtime polarity detection disagrees with
the resolved Stylix polarity, the wrong row is selected. Visible
as dark colours under a light terminal mode and vice versa.
Other modules referencing polarity (unaudited). Likely affected
wherever a module conditions on polarity == "light" / "dark" and
silently falls through on "either":
Same root cause (no resolved-polarity primitive) for both mechanisms;
new modules adopting either pattern inherit the trap unless the
invariant is fixed centrally.
Root cause
Two layers fail to resolve the value:
Generator (palette-generator/Stylix/Palette.hs). The fitness
function uses min lightScheme darkScheme per candidate but never
records which side won. The output palette.json carries base00..base0F but no polarity field.
Stylix module (stylix/palette.nix). cfg.polarity is forwarded
to consumers verbatim. There is no helper that returns the resolved polarity for the active palette.
Consumers therefore see "either" and have no way to recover the
actual polarity short of inspecting luminance themselves. This matters
because Base16 inverts base00..base07 luminance between polarities
(styling.md): any fixed slot map carries the right semantics
on only one polarity, so module fixes that hard-code a slot need to
know which polarity is active before they can pick correctly.
Reproduction
Two independent paths trigger the bug.
Generator path. A bright wallpaper scores best as a light scheme:
stylix={enable=true;image=./bright-wallpaper.jpg;# polarity left at "either" (default)};
Modules that condition on polarity == "light" see "either" and
render their dark branch over a light palette.
stylix={enable=true;base16Scheme="${tinted-schemes}/base16/one-light.yaml";# polarity left at "either" (default)};
The generator is not involved at all, but consumers still see polarity = "either" and behave incorrectly. Reported in this form by @repparw on #2288 (#2288 (comment)).
Remove "either" entirely. Implemented in treewide: replace custom color scheme generator with Tinty #2289. Forces
explicit "light" or "dark". Simplest, but breaks existing
configurations relying on the default and removes the
wallpaper-driven polarity feature.
Resolve "either" at the generator. Lift the per-polarity
scoring helpers in palette-generator/Stylix/Palette.hs to top
level, re-score after evolve returns, emit the chosen polarity in palette.json. Solves the generator path; doesn't help
user-supplied schemes. No open PR.
Expose a lib.stylix.resolvedPolarity helper. Reads from
(a) explicit cfg.polarity if not "either", (b) generator output
when (3) has landed, (c) inferred from base00 vs base07
luminance otherwise. Branch (c) covers every case independently, so
(4) is shippable before (3); (3) only refines accuracy by replacing
inference with the generator's own answer. Replaces per-module
workarounds with a central primitive. No open PR.
Implementation note: the helper must read cfg.generated.polarity
from palette.json directly. It cannot ride on lib.stylix.colors
because base16.nix's mkSchemeAttrs accepts foreign attrs without
throwing but silently drops them — colors.nix builds the result
from the normalised baseXX map, and the wrapping populatedColors // allMeta merge does not carry polarity
either. So lib.stylix.colors.polarity will be undefined regardless
of what the JSON contains.
The combination most consistent with the existing roadmap is 3 + 4:
the generator records its choice, the helper exposes it, modules drop
their per-instance workarounds. If #2289 lands first and removes "either", only path (4)'s user-supplied branch remains relevant.
Forward-compat with the generator question (#892, #2022): the helper
in (4) is the right interface regardless of which generator wins.
Only cfg.generated.polarity's source changes — Matugen or tinty
report polarity differently than the in-tree Haskell generator, but
modules that consume lib.stylix.resolvedPolarity keep working
unchanged. (4) therefore stays the right primitive even if (3)'s
specific implementation is later replaced.
Forward work
Testbeds covering the four corners. The cross-product that
catches every reproducible failure path:
dark-default: polarity = "either", no image (default
fallback).
dark-image: polarity = "either", dark wallpaper (generator
picks dark).
Summary
stylix.polarity = "either"is the default, yet no layer of Stylixresolves it to a concrete
"light"or"dark". Each consuming moduleis left to decide on its own: some treat
"either"as"dark", someemit invalid output, some pick a variant arbitrarily. Users on default
settings see silently broken or inverted rendering whenever the resolved
palette is light, whether the generator chose a light scheme from the
wallpaper or the user supplied a light
base16Schememanually.This issue is a tracker. Fixes are landing per-module ad hoc
(#2192, #2288); a wider direction is gestured at across #892 and #2289
but no consolidated approach exists.
Affected modules
Two distinct failure mechanisms share a single root cause.
Mechanism A: literal-token interpolation. The module embeds
polaritydirectly into output where only"light"or"dark"isvalid.
"either"produces a token nothing matches.obsidian(modules/obsidian/hm.nix:40, 66).theme-either; theme silentlyfails to apply.
Mechanism B: branch-selection desync. The module supplies both
polarity branches and lets the consumer pick at runtime. The consumer's
runtime signal (terminal background detection, system theme) can
disagree with Stylix's actually-resolved polarity, selecting the wrong
row.
opencode(modules/opencode/hm.nix)the resolved Stylix polarity, the wrong row is selected. Visible
as dark colours under a light terminal mode and vice versa.
Other modules referencing
polarity(unaudited). Likely affectedwherever a module conditions on
polarity == "light"/"dark"andsilently falls through on
"either":dunst/hm.nix,fnott/hm.nix,foot/hm.nix,fuzzel/hm.nixglance/{hm,nixos}.nix,gnome/{hm,nixos}.nixjjui/hm.nix,kubecolor/hm.nixqt/{hm,nixos}.nix,qutebrowser/hm.nixregreet/nixos.nix,vicinae/hm.nixSame root cause (no resolved-polarity primitive) for both mechanisms;
new modules adopting either pattern inherit the trap unless the
invariant is fixed centrally.
Root cause
Two layers fail to resolve the value:
palette-generator/Stylix/Palette.hs). The fitnessfunction uses
min lightScheme darkSchemeper candidate but neverrecords which side won. The output
palette.jsoncarriesbase00..base0Fbut no polarity field.stylix/palette.nix).cfg.polarityis forwardedto consumers verbatim. There is no helper that returns the
resolved polarity for the active palette.
Consumers therefore see
"either"and have no way to recover theactual polarity short of inspecting luminance themselves. This matters
because Base16 inverts
base00..base07luminance between polarities(styling.md): any fixed slot map carries the right semantics
on only one polarity, so module fixes that hard-code a slot need to
know which polarity is active before they can pick correctly.
Reproduction
Two independent paths trigger the bug.
Generator path. A bright wallpaper scores best as a light scheme:
Modules that condition on
polarity == "light"see"either"andrender their dark branch over a light palette.
User-supplied path. Manual light
base16Scheme, default polarity:The generator is not involved at all, but consumers still see
polarity = "either"and behave incorrectly. Reported in this form by@repparw on #2288 (#2288 (comment)).
User reports
renders dark instead of white.
opencode: fix theme polarity for light base16 schemes #2192 (comment)
tokyodark-terminalscheme with default polarity.obsidian: include both light and dark themes when polarity is either #2288 (comment)
"either"testbeds to catch thisclass of bug.
obsidian: include both light and dark themes when polarity is either #2288 (review)
Related work
treewide: replace custom color scheme generator with Matugen.Description acknowledges "polarity doesn't work like it's supposed to
('either' defaults to black)".
tinty: move from the haskell generator to tinty as a first step. Removes the"either"option as "no longer corresponds toa functional mode of the palette generator."
palette: add option to allow swapping colorscheme generator logic. Step 2 of the roadmap below.opencode: fix theme polarity for light base16 schemes.Per-module workaround.
obsidian: include both light and dark themes when polarity is either. Per-module workaround.feature: Matugen / Material You Modules. Material You modulemigration tracker.
treewide: new base16 generator. Replaces the in-tree Haskellpalette generator with an out-of-tree package.
treewide: replace custom color scheme generator with Matugen #892 (comment)
Fix directions
Listed local to architectural. Options 1 and 2 address only the
generator path; 3 and 4 cover the user-supplied path as well.
Per-module workarounds. Each module decides locally how to
handle
"either"(currently opencode: fix theme polarity for light base16 schemes #2192, obsidian: include both light and dark themes when polarity is either #2288). Status quo. Doesn'tscale; new modules inherit the trap; user-supplied light schemes
stay broken on modules without a workaround.
Remove
"either"entirely. Implemented in treewide: replace custom color scheme generator with Tinty #2289. Forcesexplicit
"light"or"dark". Simplest, but breaks existingconfigurations relying on the default and removes the
wallpaper-driven polarity feature.
Resolve
"either"at the generator. Lift the per-polarityscoring helpers in
palette-generator/Stylix/Palette.hsto toplevel, re-score after
evolvereturns, emit the chosen polarity inpalette.json. Solves the generator path; doesn't helpuser-supplied schemes. No open PR.
Expose a
lib.stylix.resolvedPolarityhelper. Reads from(a) explicit
cfg.polarityif not"either", (b) generator outputwhen (3) has landed, (c) inferred from
base00vsbase07luminance otherwise. Branch (c) covers every case independently, so
(4) is shippable before (3); (3) only refines accuracy by replacing
inference with the generator's own answer. Replaces per-module
workarounds with a central primitive. No open PR.
Implementation note: the helper must read
cfg.generated.polarityfrom
palette.jsondirectly. It cannot ride onlib.stylix.colorsbecause base16.nix's
mkSchemeAttrsaccepts foreign attrs withoutthrowing but silently drops them —
colors.nixbuilds the resultfrom the normalised
baseXXmap, and the wrappingpopulatedColors // allMetamerge does not carrypolarityeither. So
lib.stylix.colors.polaritywill be undefined regardlessof what the JSON contains.
Framing concern: @trueNAHO has questioned whether Stylix should
override polarity at all when consumers have native mechanisms
(opencode: fix theme polarity for light base16 schemes #2192 (review)).
The combination most consistent with the existing roadmap is 3 + 4:
the generator records its choice, the helper exposes it, modules drop
their per-instance workarounds. If #2289 lands first and removes
"either", only path (4)'s user-supplied branch remains relevant.Forward-compat with the generator question (#892, #2022): the helper
in (4) is the right interface regardless of which generator wins.
Only
cfg.generated.polarity's source changes — Matugen or tintyreport polarity differently than the in-tree Haskell generator, but
modules that consume
lib.stylix.resolvedPolaritykeep workingunchanged. (4) therefore stays the right primitive even if (3)'s
specific implementation is later replaced.
Forward work
catches every reproducible failure path:
dark-default:polarity = "either", no image (defaultfallback).
dark-image:polarity = "either", dark wallpaper (generatorpicks dark).
light-image:polarity = "either", bright wallpaper(generator picks light; reproduces the obsidian/qt-bevel bugs).
manual-light-scheme:polarity = "either",base16Scheme = one-light.yaml(reproduces opencode: fix theme polarity for light base16 schemes #2192 / obsidian: include both light and dark themes when polarity is either #2288'suser-supplied path; the generator is not involved).
Suggested originally by @TheColorman on obsidian: include both light and dark themes when polarity is either #2288.
lib.stylix.resolvedPolarity ∈ {"light", "dark"}for everyconfiguration path (default, image, manual scheme,
explicit polarity).
scheme's actual luminance ordering.
CSS class is the cheapest) showing it no longer emits
.theme-eitherunder any testbed.showing the row marked active matches the resolved polarity.