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 ## Acceptance criteria
- [ ] Stylix is a flake input, wired into the host build. - [x] 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. - [x] 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. - [x] 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. - [x] 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] 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": { "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": { "chaotic": {
"inputs": { "inputs": {
"flake-schemas": "flake-schemas", "flake-schemas": "flake-schemas",
@@ -41,6 +109,22 @@
"type": "github" "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": { "flake-parts": {
"inputs": { "inputs": {
"nixpkgs-lib": [ "nixpkgs-lib": [
@@ -62,6 +146,27 @@
"type": "github" "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": { "flake-schemas": {
"locked": { "locked": {
"lastModified": 1780327564, "lastModified": 1780327564,
@@ -76,6 +181,22 @@
"url": "https://flakehub.com/f/DeterminateSystems/flake-schemas/%3D0.5.0.tar.gz" "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": { "gitea-axi": {
"inputs": { "inputs": {
"home-manager": "home-manager_2", "home-manager": "home-manager_2",
@@ -97,6 +218,25 @@
"url": "https://git.alexion.dev/alexion/gitea-axi" "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": { "home-manager": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -265,6 +405,31 @@
"type": "github" "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": { "root": {
"inputs": { "inputs": {
"chaotic": "chaotic", "chaotic": "chaotic",
@@ -276,7 +441,8 @@
"nixpkgs-stable": "nixpkgs-stable", "nixpkgs-stable": "nixpkgs-stable",
"nixpkgs-unstable": "nixpkgs-unstable", "nixpkgs-unstable": "nixpkgs-unstable",
"nixvim": "nixvim", "nixvim": "nixvim",
"sops-nix": "sops-nix" "sops-nix": "sops-nix",
"stylix": "stylix"
} }
}, },
"sops-nix": { "sops-nix": {
@@ -299,6 +465,39 @@
"type": "github" "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": { "systems": {
"locked": { "locked": {
"lastModified": 1774449309, "lastModified": 1774449309,
@@ -314,6 +513,85 @@
"repo": "default", "repo": "default",
"type": "github" "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", "root": "root",

View File

@@ -40,6 +40,13 @@
inputs.nixpkgs.follows = "nixpkgs"; 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 # Agent-ergonomic CLI for Gitea, with its home-manager module wiring in the
# Claude Code context where that harness is present. # Claude Code context where that harness is present.
gitea-axi = { gitea-axi = {

View File

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

View File

@@ -12,5 +12,6 @@ in
modules.desktop.hyprland.enable = lib.mkDefault true; modules.desktop.hyprland.enable = lib.mkDefault true;
modules.desktop.login.enable = lib.mkDefault true; modules.desktop.login.enable = lib.mkDefault true;
modules.desktop.terminal.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;

What's the point of Stylix if I still need hand written themes...?

What's the point of Stylix if I still need hand written themes...?

Fair challenge. Stylix's payoff here isn't the terminal — it's everything that had no theme: GTK apps, Qt apps, the cursor, and the bar / launcher / lock / notifications still to come. That's roughly a dozen surfaces driven from one scheme so they can't drift apart. The terminal is the one place we already had curated Nord, so it's the one place Stylix adds least.

Per tool:

  • nvimgbprod/nord.nvim, a purpose-built colorscheme with treesitter-aware highlights. Stylix's neovim target would swap that for a generic 16-colour base16 mapping — a downgrade, not a win.
  • tmux → hand-placed Nord hex in the status bar (#2E3440 / #88C0D0 / …), already exactly as wanted.
  • fish → no real colour theme to protect; it rides the terminal palette.

And it's reversible per target — each is a one-line enable = true — so nothing is locked in. If you want to shrink the hand-theme surface, tmux and fish are the safe ones to hand to Stylix; I'd keep nvim hand-themed, since the dedicated colorscheme beats the base16 mapping. Say the word and I'll flip whichever you want.

— Claude

Fair challenge. Stylix's payoff here isn't the terminal — it's everything that had *no* theme: GTK apps, Qt apps, the cursor, and the bar / launcher / lock / notifications still to come. That's roughly a dozen surfaces driven from one scheme so they can't drift apart. The terminal is the one place we *already* had curated Nord, so it's the one place Stylix adds least. Per tool: - **nvim** → `gbprod/nord.nvim`, a purpose-built colorscheme with treesitter-aware highlights. Stylix's neovim target would swap that for a generic 16-colour base16 mapping — a downgrade, not a win. - **tmux** → hand-placed Nord hex in the status bar (`#2E3440` / `#88C0D0` / …), already exactly as wanted. - **fish** → no real colour theme to protect; it rides the terminal palette. And it's reversible per target — each is a one-line `enable = true` — so nothing is locked in. If you want to shrink the hand-theme surface, **tmux and fish are the safe ones to hand to Stylix**; I'd keep **nvim** hand-themed, since the dedicated colorscheme beats the base16 mapping. Say the word and I'll flip whichever you want. — Claude

Only nvim shouldn't be managed by stylix

Only nvim shouldn't be managed by stylix
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;
};
};
};
}