feat(desktop): theme the graphical layer Nord with Stylix (task 0022) #14

Merged
alexion merged 2 commits from task-0022-stylix-nord-theming into main 2026-07-22 11:45:06 -04:00
6 changed files with 384 additions and 6 deletions
Showing only changes of commit 27a28b1ae7 - Show all commits

View File

@@ -13,8 +13,34 @@ The theming is reversible per target, so individual surfaces can migrate toward
## Acceptance criteria
- [ ] Stylix is a flake input, wired into the host build.
- [ ] A desktop theming module resolves a single Nord base16 scheme and applies it to GTK, Qt, cursor, and system fonts.
- [ ] A single static Nord wallpaper is set by Stylix; no dynamic, animated, or cycling wallpaper.
- [ ] Stylix targets for nvim, tmux, and fish are off, leaving their existing themes untouched.
- [ ] neogaia builds green under `nix flake check`, and an eval probe confirms the resolved scheme is Nord.
- [x] Stylix is a flake input, wired into the host build.
- [x] A desktop theming module resolves a single Nord base16 scheme and applies it to GTK, Qt, cursor, and system fonts.
- [x] A single static Nord wallpaper is set by Stylix; no dynamic, animated, or cycling wallpaper.
- [x] Stylix targets for nvim, tmux, and fish are off, leaving their existing themes untouched.
- [x] neogaia builds green under `nix flake check`, and an eval probe confirms the resolved scheme is Nord.
## Implementation Notes
- **Wallpaper is a generated Nord gradient, not a shipped image.**
Stylix requires an `image`, and the spec asks only for "a single static Nord wallpaper".
Rather than commit a binary blob or fetch one over the network at build time, the module draws a vertical gradient across the Nord Polar Night shades (`#2E3440``#3B4252`) with ImageMagick.
It is static, genuinely Nord, and fully reproducible with no external dependency beyond a cached build tool.
Swapping in a picture later is a one-line change to `image`.
- **The nvim target is `nixvim`, not `neovim`.**
nvim here is configured through nixvim, so the Stylix target that would theme it is `nixvim`.
Disabling `neovim` would have been a no-op and left nvim themed.
- **fish is disabled at two levels.**
fish is enabled both at system level (`programs.fish`, for `/etc/shells` and vendor completions) and in home-manager, so its theming has a target in each scope.
Both are turned off, so the hand-written fish theme stands.
- **Cursor generation switched on explicitly.**
home-manager now wants `home.pointerCursor.enable` set explicitly rather than inferring it from the presence of cursor settings, so the module sets it to silence the deprecation and keep the build warning-clean (bar the pre-existing benign nixvim `nixpkgs.follows` notice).
- **System font pinned to JetBrains Mono.**
The acceptance criterion asks for "system fonts" without naming one, so the monospace is pinned to JetBrains Mono and the serif/sans/emoji families are left at Stylix's Nord-coherent defaults.
- **Stylix module imported unconditionally.**
`lib.nix` adds `inputs.stylix.nixosModules.stylix` to every host's module set, matching how the other input modules are wired.
It stays inert until `stylix.enable` is set, which only the theming module does, only when the desktop is on.

280
flake.lock generated
View File

@@ -1,5 +1,73 @@
{
"nodes": {
"base16": {
"inputs": {
"fromYaml": "fromYaml"
},
"locked": {
"lastModified": 1755819240,
"narHash": "sha256-qcMhnL7aGAuFuutH4rq9fvAhCpJWVHLcHVZLtPctPlo=",
"owner": "SenchoPens",
"repo": "base16.nix",
"rev": "75ed5e5e3fce37df22e49125181fa37899c3ccd6",
"type": "github"
},
"original": {
"owner": "SenchoPens",
"repo": "base16.nix",
"type": "github"
}
},
"base16-fish": {
"flake": false,
"locked": {
"lastModified": 1765809053,
"narHash": "sha256-XCUQLoLfBJ8saWms2HCIj4NEN+xNsWBlU1NrEPcQG4s=",
"owner": "tomyun",
"repo": "base16-fish",
"rev": "86cbea4dca62e08fb7fd83a70e96472f92574782",
"type": "github"
},
"original": {
"owner": "tomyun",
"repo": "base16-fish",
"rev": "86cbea4dca62e08fb7fd83a70e96472f92574782",
"type": "github"
}
},
"base16-helix": {
"flake": false,
"locked": {
"lastModified": 1776754714,
"narHash": "sha256-E3OAK27smtATTmX45uoTSRsVD+Y+ZiVVfgM/tjpbtYg=",
"owner": "tinted-theming",
"repo": "base16-helix",
"rev": "4d508123037e7851ad36ebf7d9c48b0e9e1eb581",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "base16-helix",
"type": "github"
}
},
"base16-vim": {
"flake": false,
"locked": {
"lastModified": 1732806396,
"narHash": "sha256-e0bpPySdJf0F68Ndanwm+KWHgQiZ0s7liLhvJSWDNsA=",
"owner": "tinted-theming",
"repo": "base16-vim",
"rev": "577fe8125d74ff456cf942c733a85d769afe58b7",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "base16-vim",
"rev": "577fe8125d74ff456cf942c733a85d769afe58b7",
"type": "github"
}
},
"chaotic": {
"inputs": {
"flake-schemas": "flake-schemas",
@@ -41,6 +109,22 @@
"type": "github"
}
},
"firefox-gnome-theme": {
"flake": false,
"locked": {
"lastModified": 1782007937,
"narHash": "sha256-PbnJr+eB+9Czol3ReI83dUgEhcn0sDK6TSy6ODTQm88=",
"owner": "rafaelmardojai",
"repo": "firefox-gnome-theme",
"rev": "981bd332015397fb1ca033fa982bd61635160c78",
"type": "github"
},
"original": {
"owner": "rafaelmardojai",
"repo": "firefox-gnome-theme",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
@@ -62,6 +146,27 @@
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": [
"stylix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1782949081,
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-schemas": {
"locked": {
"lastModified": 1780327564,
@@ -76,6 +181,22 @@
"url": "https://flakehub.com/f/DeterminateSystems/flake-schemas/%3D0.5.0.tar.gz"
}
},
"fromYaml": {
"flake": false,
"locked": {
"lastModified": 1731966426,
"narHash": "sha256-lq95WydhbUTWig/JpqiB7oViTcHFP8Lv41IGtayokA8=",
"owner": "SenchoPens",
"repo": "fromYaml",
"rev": "106af9e2f715e2d828df706c386a685698f3223b",
"type": "github"
},
"original": {
"owner": "SenchoPens",
"repo": "fromYaml",
"type": "github"
}
},
"gitea-axi": {
"inputs": {
"home-manager": "home-manager_2",
@@ -97,6 +218,25 @@
"url": "https://git.alexion.dev/alexion/gitea-axi"
}
},
"gnome-shell": {
"flake": false,
"locked": {
"host": "gitlab.gnome.org",
"lastModified": 1776175984,
"narHash": "sha256-RJFlFW8GiMei6oqUGrMkGEvVqOH8U7Q8abc1yK4VKD8=",
"owner": "GNOME",
"repo": "gnome-shell",
"rev": "e0fdc4c13250e9a9b8ea9594c83925274f4a5dca",
"type": "gitlab"
},
"original": {
"host": "gitlab.gnome.org",
"owner": "GNOME",
"ref": "50.1",
"repo": "gnome-shell",
"type": "gitlab"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
@@ -265,6 +405,31 @@
"type": "github"
}
},
"nur": {
"inputs": {
"flake-parts": [
"stylix",
"flake-parts"
],
"nixpkgs": [
"stylix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1783439237,
"narHash": "sha256-WUr8JF2v3n4Y30E5dxv4sAgNJXpVDBoCQNoQ/V4+n4o=",
"owner": "nix-community",
"repo": "NUR",
"rev": "b70bb66c7bcd162642f3a609bc16843c7059f503",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "NUR",
"type": "github"
}
},
"root": {
"inputs": {
"chaotic": "chaotic",
@@ -276,7 +441,8 @@
"nixpkgs-stable": "nixpkgs-stable",
"nixpkgs-unstable": "nixpkgs-unstable",
"nixvim": "nixvim",
"sops-nix": "sops-nix"
"sops-nix": "sops-nix",
"stylix": "stylix"
}
},
"sops-nix": {
@@ -299,6 +465,39 @@
"type": "github"
}
},
"stylix": {
"inputs": {
"base16": "base16",
"base16-fish": "base16-fish",
"base16-helix": "base16-helix",
"base16-vim": "base16-vim",
"firefox-gnome-theme": "firefox-gnome-theme",
"flake-parts": "flake-parts_2",
"gnome-shell": "gnome-shell",
"nixpkgs": [
"nixpkgs"
],
"nur": "nur",
"systems": "systems_2",
"tinted-kitty": "tinted-kitty",
"tinted-schemes": "tinted-schemes",
"tinted-tmux": "tinted-tmux",
"tinted-zed": "tinted-zed"
},
"locked": {
"lastModified": 1784676123,
"narHash": "sha256-ndyanKzw90yX2nUVFmTuYqXidUNymtMfgmIHyNdhht0=",
"owner": "danth",
"repo": "stylix",
"rev": "66714e5ce44269ecc58c20d9196da8dbe1b27a31",
"type": "github"
},
"original": {
"owner": "danth",
"repo": "stylix",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1774449309,
@@ -314,6 +513,85 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"tinted-kitty": {
"flake": false,
"locked": {
"lastModified": 1735730497,
"narHash": "sha256-4KtB+FiUzIeK/4aHCKce3V9HwRvYaxX+F1edUrfgzb8=",
"owner": "tinted-theming",
"repo": "tinted-kitty",
"rev": "de6f888497f2c6b2279361bfc790f164bfd0f3fa",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "tinted-kitty",
"type": "github"
}
},
"tinted-schemes": {
"flake": false,
"locked": {
"lastModified": 1781968807,
"narHash": "sha256-yYO3Vw2M0y3TAUqt+9+Mj0zwP3XDTF5/PXcPhhFQ1ZM=",
"owner": "tinted-theming",
"repo": "schemes",
"rev": "2ccef2f4b22e3cab5a9292811f7133a07eeba4a7",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "schemes",
"type": "github"
}
},
"tinted-tmux": {
"flake": false,
"locked": {
"lastModified": 1782012462,
"narHash": "sha256-2iDiD8DQLwS1lGuD9TS8WlvNyDoTs6krWntJbtB2zGo=",
"owner": "tinted-theming",
"repo": "tinted-tmux",
"rev": "8c4e750f738a742bd73377ee41d3dadedebedef4",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "tinted-tmux",
"type": "github"
}
},
"tinted-zed": {
"flake": false,
"locked": {
"lastModified": 1782009766,
"narHash": "sha256-VUhBjpGvWqHI7rWeyMYb/u87YJSXHKfVV6S+IelWeO8=",
"owner": "tinted-theming",
"repo": "base16-zed",
"rev": "5e8350bcd354e3241ab681a265fa6ef060c40be1",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "base16-zed",
"type": "github"
}
}
},
"root": "root",

View File

@@ -40,6 +40,13 @@
inputs.nixpkgs.follows = "nixpkgs";
};
# Themes the graphical layer from one base16 scheme.
# Follows our nixpkgs so it themes the same package set the host builds.
stylix = {
url = "github:danth/stylix";
inputs.nixpkgs.follows = "nixpkgs";
};
# Agent-ergonomic CLI for Gitea, with its home-manager module wiring in the
# Claude Code context where that harness is present.
gitea-axi = {

View File

@@ -51,6 +51,7 @@ let
inputs.chaotic.nixosModules.default
inputs.disko.nixosModules.disko
inputs.sops-nix.nixosModules.sops
inputs.stylix.nixosModules.stylix
(self + "/system.nix")
(self + "/hosts/${hostName}")
{ networking.hostName = hostName; }

View File

@@ -12,5 +12,6 @@ in
modules.desktop.hyprland.enable = lib.mkDefault true;
modules.desktop.login.enable = lib.mkDefault true;
modules.desktop.terminal.enable = lib.mkDefault true;
modules.desktop.theming.enable = lib.mkDefault true;
};
}

View File

@@ -0,0 +1,65 @@
{
config,
lib,
pkgs,
...
}:
# Nord theming for the graphical layer, driven from one base16 scheme by Stylix.
let
cfg = config.modules.desktop.theming;
user = config.user.name;
# A single static Nord wallpaper, drawn as a vertical gradient across the
# Polar Night shades so it needs no committed binary and no network fetch.
wallpaper = pkgs.runCommand "nord-wallpaper.png" { } ''
${pkgs.imagemagick}/bin/magick \
-size 2560x1440 gradient:'#2E3440'-'#3B4252' \
"$out"
'';
in
{
options.modules.desktop.theming.enable =
lib.mkEnableOption "Nord theming of the graphical layer via Stylix";
config = lib.mkIf cfg.enable {
stylix = {
enable = true;
# Nord is a dark scheme.
# The polarity steers Stylix's contrast choices.
polarity = "dark";
base16Scheme = "${pkgs.base16-schemes}/share/themes/nord.yaml";
image = wallpaper;
cursor = {
package = pkgs.bibata-cursors;
name = "Bibata-Modern-Ice";
size = 24;
};
fonts.monospace = {
package = pkgs.jetbrains-mono;
name = "JetBrains Mono";
};
# fish is also installed at system level, so its theming has a target
# here of its own.
targets.fish.enable = false;
};
home-manager.users.${user} = {
# Stylix drives the cursor through home-manager's pointer-cursor config,
# whose generation must be switched on explicitly.
home.pointerCursor.enable = true;
# nvim, tmux, and fish are configured through home-manager and keep their
# own hand-written themes, so their Stylix targets stay off.
stylix.targets = {
fish.enable = false;
alexion marked this conversation as resolved
Review

Done in 6d1afa4. Only nixvim stays off now; tmux and fish are Stylix-managed.

fish had no colour theme of its own, so that was a clean flip. tmux did — a hand-written Nord status bar in extra.conf — which would have fought Stylix, so I removed those colour lines and let Stylix theme the status and pane styles. I kept your minimal layout (session name + window list, empty right side) and it's reapplied after Stylix's theme, so the layout still wins while the colours come from the one scheme.

Verified on the rendered tmux.conf: Stylix sources base16-nord.conf before your layout block, its unconditional colour lines apply, and its optional verbose statusbar stays gated off — so no clobbering. nix flake check is green.

— Claude

Done in 6d1afa4. Only `nixvim` stays off now; tmux and fish are Stylix-managed. fish had no colour theme of its own, so that was a clean flip. tmux did — a hand-written Nord status bar in `extra.conf` — which would have fought Stylix, so I removed those colour lines and let Stylix theme the status and pane styles. I kept your minimal layout (session name + window list, empty right side) and it's reapplied after Stylix's theme, so the layout still wins while the colours come from the one scheme. Verified on the rendered `tmux.conf`: Stylix sources `base16-nord.conf` before your layout block, its unconditional colour lines apply, and its optional verbose statusbar stays gated off — so no clobbering. `nix flake check` is green. — Claude
tmux.enable = false;
nixvim.enable = false;
};
};
};
}