Skip to content

Tracking: resolve stylix.polarity = "either" to a concrete polarity #2301

Description

@Bad3r

Summary

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.

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)

Other modules referencing polarity (unaudited). Likely affected
wherever a module conditions on polarity == "light" / "dark" and
silently falls through on "either":

  • dunst/hm.nix, fnott/hm.nix, foot/hm.nix, fuzzel/hm.nix
  • glance/{hm,nixos}.nix, gnome/{hm,nixos}.nix
  • jjui/hm.nix, kubecolor/hm.nix
  • qt/{hm,nixos}.nix, qutebrowser/hm.nix
  • regreet/nixos.nix, vicinae/hm.nix

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:

  1. 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.
  2. 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.

User-supplied path. Manual light base16Scheme, default polarity:

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)).

User reports

Related work

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.

  1. 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't
    scale; new modules inherit the trap; user-supplied light schemes
    stay broken on modules without a workaround.

  2. 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.

  3. 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.

  4. 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.

    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 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:
    1. dark-default: polarity = "either", no image (default
      fallback).
    2. dark-image: polarity = "either", dark wallpaper (generator
      picks dark).
    3. light-image: polarity = "either", bright wallpaper
      (generator picks light; reproduces the obsidian/qt-bevel bugs).
    4. 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's
      user-supplied path; the generator is not involved).
      Suggested originally by @TheColorman on obsidian: include both light and dark themes when polarity is either #2288.
  • Acceptance criteria for the helper:
    • lib.stylix.resolvedPolarity ∈ {"light", "dark"} for every
      configuration path (default, image, manual scheme,
      explicit polarity).
    • On the four testbeds above, the resolved polarity matches the
      scheme's actual luminance ordering.
    • Snapshot test on at least one Mechanism-A module (obsidian's
      CSS class is the cheapest) showing it no longer emits
      .theme-either under any testbed.
    • Snapshot test on at least one Mechanism-B module (opencode)
      showing the row marked active matches the resolved polarity.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions