# Stylix — themes the long tail of applications (GTK, Qt, cursors, # fonts) from the same state.json that drives the live engine. # # Division of labour: the hot-reload trio (Hyprland, Waybar, Kitty) # is owned by the Nomarchy engine and updates instantly; everything # Stylix touches updates on the next home-manager switch. That is why # autoEnable is off and the trio's Stylix targets stay disabled. # # Note: the stylix home module itself is imported by homeModules.nomarchy # in flake.nix (it needs the stylix flake input). { config, lib, pkgs, ... }: let cfg = config.nomarchy; t = cfg.theme; c = t.colors; hex = lib.removePrefix "#"; # Accent-button chips (#130). The label is the palette's base — dark on a # dark theme, cream on a light one — so the chip has to be pulled AWAY from # the label until the label is legible: toward white when the label is dark, # toward black when it is cream. Same factor both ways; only the anchor # flips, so "solid accent button, base label" stays the design and just # shifts tone. # # Why a nudge at all: on a saturated mid-tone accent NOTHING clears 4.5 — # neither the cream base (summer-day: 2.72) nor the dark text (1.65) — which # is why upstream adw-gtk3 ships white-on-accent at ~2.7 and why "darken the # label" was a dead end. The chip must move. # # 0.70 measured across all 24 palettes (tools/check-theme-contrast.py asserts # it): worst dark 4.86 (nord/bad), worst light 5.59 (summer-day). Today's 0.90 # fails seven themes — miasma/bad 3.43, rose-pine 2.70 — so this is not only a # light-theme fix, which is what the item assumed. # # GTK's mix(a, b, f) = a + (b - a) * f (gtkcsscolorvalue.c), i.e. f is the # weight of the SECOND colour: 0.70 = 70% accent, 30% anchor. Read it that way # or the ladder below looks inverted. chipAnchor = if t.mode == "light" then "black" else "white"; # Each state pulls further from the label, so contrast only ever improves — # rest is the worst case and the one the check pins. chip = role: f: "mix(${chipAnchor}, ${role}, ${f})"; # Map the Nomarchy palette onto base16 roles. base16 = { base00 = hex c.base; # default background base01 = hex c.mantle; # darker background (status bars) base02 = hex c.surface; # selection background base03 = hex c.muted; # comments base04 = hex c.subtext; # dark foreground base05 = hex c.text; # default foreground base06 = hex c.text; # light foreground base07 = hex c.text; # lightest foreground base08 = hex c.bad; # red base09 = hex c.warn; # orange base0A = hex c.warn; # yellow base0B = hex c.good; # green base0C = hex (builtins.elemAt t.ansi 6); # cyan base0D = hex c.accent; # blue base0E = hex c.accentAlt;# magenta base0F = hex c.bad; # brown/deprecated }; in { config = lib.mkIf cfg.stylix.enable { stylix = { enable = true; autoEnable = false; # explicit targets only — the engine owns the rest polarity = if t.mode == "light" then "light" else "dark"; base16Scheme = base16; targets = { gtk = { enable = true; # nm-applet / classic GTK menus: Stylix base16 selection can # collapse into surface/muted on dark themes (Boreal: selected # row and submenu arrows were effectively invisible). High- # contrast selected rows + explicit arrow colour. Appended to # Stylix's generated gtk.css (not gtk.gtk3.extraCss — that is a # no-op under Stylix). # # #98: dialog button labels. Stylix always installs theme name # "adw-gtk3" (light sheet) and only recolors @define-color vars. # The light sheet hardcodes dark/black button text in places; on a # dark palette that becomes black-on-dark in GTK dialogs. We force # adw-gtk3-dark for dark mode (below) and still pin button label # colours here so dialogs stay legible if a light sheet slips in. extraCss = '' menu menuitem, .menu menuitem, .context-menu menuitem { color: #${base16.base05}; } menu menuitem:hover, menu menuitem:selected, .menu menuitem:hover, .menu menuitem:selected, .context-menu menuitem:hover, .context-menu menuitem:selected, menubar > menuitem:hover { background-color: #${base16.base0D}; color: #${base16.base00}; } menu menuitem:disabled, .menu menuitem:disabled { color: #${base16.base03}; } /* Submenu disclosure arrows need no icon remap: pan-end-symbolic resolves via Papirus-Dark's breeze-dark inheritance. The "invisible arrow" bug was Waybar's process-wide stylesheet: its `* { min-height: 0; }` reset also hits the SNI tray menus Waybar hosts and collapses arrow/check nodes. NB: this file CANNOT fix that — Waybar adds its provider at USER priority too and wins the tie in practice (GTK 3.24, observed), so every waybar.css carries its own menu counter-rules. The rules here cover menus in ordinary GTK apps only. */ menu menuitem arrow, .menu menuitem arrow, .context-menu menuitem arrow { min-width: 16px; min-height: 16px; } check, radio { min-width: 14px; min-height: 14px; color: inherit; } /* GTK4 / popover menus (some portals). */ popover.menu contents modelbutton { color: #${base16.base05}; } popover.menu contents modelbutton:hover, popover.menu contents modelbutton:selected { background-color: #${base16.base0D}; color: #${base16.base00}; } /* Dialog / message-box action buttons (#98). */ button, button label, .dialog-action-area button, .dialog-action-area button label, messagedialog button, messagedialog button label, .message-dialog button, .message-dialog button label { color: #${base16.base05}; } button:disabled, button:disabled label { color: alpha(#${base16.base05}, 0.5); } /* Accent buttons: the palette's base as the label, on a chip pulled away from it (#130 — see `chip` above for the why and the measured numbers). BOTH chips are pinned, not just destructive as in #98. Two reasons, and the second is the durable one: 1. adw-gtk3 builds the destructive background from currentColor — mix(@destructive_color, alpha(currentColor,…)) — so pinning the label alone drags the background with it and the button renders dark-on-dark (1.03:1, invisible). That is the #98 bug. 2. A pinned label on an UNPINNED background is unknowable: no static check can see what it renders on, which is exactly why every check was green while #98 shipped. A pinned pair is arithmetic, and tools/check-theme-contrast.py now asserts it for all 24 palettes. The rule for anything added here: pin the pair, or pin neither. Enabled only — :disabled keeps the sheet's own chrome and already renders legibly. .default is GTK's dialog default and adw-gtk3 styles it as an accent button, which is why it shares the suggested chip rather than inheriting a plain one. */ button.suggested-action, button.suggested-action label, button.destructive-action, button.destructive-action label, button.default, button.default label { color: #${base16.base00}; } button.suggested-action:not(.flat):not(:disabled), button.default:not(.flat):not(:disabled) { background-color: ${chip "@accent_bg_color" "0.70"}; } button.suggested-action:not(.flat):not(:disabled):hover, button.default:not(.flat):not(:disabled):hover { background-color: ${chip "@accent_bg_color" "0.65"}; } button.suggested-action:not(.flat):not(:disabled):active, button.suggested-action:not(.flat):not(:disabled):checked, button.default:not(.flat):not(:disabled):active, button.default:not(.flat):not(:disabled):checked { background-color: ${chip "@accent_bg_color" "0.55"}; } button.destructive-action:not(.flat):not(:disabled) { background-color: ${chip "@destructive_bg_color" "0.70"}; } button.destructive-action:not(.flat):not(:disabled):hover { background-color: ${chip "@destructive_bg_color" "0.65"}; } button.destructive-action:not(.flat):not(:disabled):active, button.destructive-action:not(.flat):not(:disabled):checked { background-color: ${chip "@destructive_bg_color" "0.55"}; } button.suggested-action.flat, button.suggested-action.flat label { color: #${base16.base0D}; } button.destructive-action.flat, button.destructive-action.flat label { color: #${base16.base08}; } ''; }; qt.enable = true; # No-op unless programs.zathura is on (viewers.nix enables it). zathura.enable = true; }; cursor = { name = lib.mkDefault "Bibata-Modern-Classic"; package = lib.mkDefault pkgs.bibata-cursors; size = lib.mkDefault 24; }; # GTK/file-manager/rofi icon theme, resolved from the JSON in # theme.nix (per-theme `icons`, else Papirus-Dark/Light by mode). # Stylix sets gtk.iconTheme from this; both dark/light point at the # already-mode-resolved name. The package is papirus by default and # only unions in another pack when a theme's `icons` names one # (theme.nix — opt-in, no default closure bloat). icons = { enable = true; package = lib.mkDefault t.iconThemePackage; dark = t.iconTheme; light = t.iconTheme; }; fonts = { monospace = { name = t.fonts.mono; package = lib.mkDefault pkgs.nerd-fonts.jetbrains-mono; }; sansSerif = { name = t.fonts.ui; package = lib.mkDefault pkgs.inter; }; serif = { name = t.fonts.ui; package = lib.mkDefault pkgs.inter; }; emoji = { name = "Noto Color Emoji"; package = lib.mkDefault pkgs.noto-fonts-color-emoji; }; sizes.terminal = t.fonts.size; }; }; # GTK3 hides menu-item icons by default (gtk-menu-images was flipped # off upstream in 3.10). Classic tray menus — nm-applet's Wi-Fi list, # udiskie — are built as image menu items, so without this their icons # silently vanish. Stylix drives the HM gtk module, so extraConfig # merges into the same settings.ini it already writes. gtk.gtk3.extraConfig = { "gtk-menu-images" = 1; # Pair with adw-gtk3-dark so the theme sheet itself is dark, not just # the @define-color recolor (see gtk.theme.name below). "gtk-application-prefer-dark-theme" = t.mode != "light"; }; gtk.gtk4.extraConfig."gtk-application-prefer-dark-theme" = t.mode != "light"; # #98: Stylix hardcodes gtk.theme.name = "adw-gtk3" (light CSS) for every # polarity and only injects libadwaita colour tokens. The light adw-gtk3 # sheet assumes light chrome and hardcodes near-black button labels in # places — on Boreal that is black text on dark dialog buttons. Force # the matching dark/light sheet; the package still provides both names. gtk.theme = { package = lib.mkDefault pkgs.adw-gtk3; name = lib.mkForce (if t.mode == "light" then "adw-gtk3" else "adw-gtk3-dark"); }; # GTK4/libadwaita and Qt6 apps decide dark vs light from the XDG # portal's color-scheme (org.freedesktop.appearance), which # xdg-desktop-portal-gtk sources from this gsettings key. Stylix sets # the GTK theme and `polarity` but not this, so a light theme still # rendered libadwaita/Qt apps dark (and vice-versa). Drive it from the # palette mode. (programs.dconf.enable is already on system-side.) dconf.settings."org/gnome/desktop/interface" = { color-scheme = if t.mode == "light" then "prefer-light" else "prefer-dark"; gtk-theme = if t.mode == "light" then "adw-gtk3" else "adw-gtk3-dark"; }; }; }