After years on Arch Linux I got tired of remembering every tweak I’d made to get my system right after a fresh install. Dotfiles helped, but they never told the full story. NixOS fixes that: if it’s not in the config, it doesn’t exist.
This is the setup I ended up with: NixOS with flakes, Home Manager, the Niri window manager, and a Wayland-first desktop.
Why Niri?
I used i3 for years. When I moved to Wayland, I tried Sway and Hyprland, but neither felt right. Niri is a scrollable tiling compositor, the windows scroll horizontally in columns instead of being split into fixed workspaces. It sounds strange but works surprisingly well once you get used to it.
The flake structure
Everything lives in ~/nixos:
flake.nix
configuration.nix
home.nix
hardware-configuration.nix
scripts/
alacritty-scratchpad.sh
vicinae/
open-nixos-config.sh
The flake.nix wires everything together:
{
description = "NixOS config";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
home-manager = {
url = "github:nix-community/home-manager/master";
inputs.nixpkgs.follows = "nixpkgs";
};
niri = {
url = "github:sodiboo/niri-flake";
inputs.nixpkgs.follows = "nixpkgs";
};
vicinae.url = "github:vicinaehq/vicinae";
noctalia = {
url = "github:noctalia-dev/noctalia-shell";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-colors.url = "github:misterio77/nix-colors";
};
outputs = inputs@{ self, nixpkgs, home-manager, niri, vicinae, noctalia, nix-colors, ... }: {
nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit inputs; };
modules = [
./configuration.nix
home-manager.nixosModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.YOUR_USER = import ./home.nix;
home-manager.extraSpecialArgs = { inherit inputs; };
home-manager.sharedModules = [
niri.homeModules.niri
vicinae.homeManagerModules.default
noctalia.homeModules.default
nix-colors.homeManagerModules.default
];
}
];
};
};
}
Flakes and Git
Flakes only see files tracked by Git. If you add a new file and wonder why the build can’t find it,
git addis almost always the answer.
System configuration
configuration.nix handles system-level things: NVIDIA drivers, Docker, portals, PipeWire.
hardware.graphics = {
enable = true;
enable32Bit = true;
};
services.xserver.videoDrivers = [ "nvidia" ];
hardware.nvidia = {
modesetting.enable = true;
open = false;
nvidiaSettings = true;
package = config.boot.kernelPackages.nvidiaPackages.stable;
};
virtualisation.docker.enable = true;
users.users.YOUR_USER.extraGroups = [ "docker" ];
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
};
xdg.portal = {
enable = true;
xdgOpenUsePortal = true;
config.common.default = "*";
extraPortals = [ pkgs.xdg-desktop-portal-gtk ];
};
XDG portal
Without the portal setup, apps like Slack can’t open links in the browser and screenshot tools won’t work. The GTK portal works well outside of GNOME sessions, but avoid the GNOME portal unless you’re running a full GNOME session.
Niri configuration
Niri is configured through Home Manager. The sodiboo flake exposes a home module that handles the package and config file generation:
programs.niri = {
enable = true;
settings = {
prefer-no-csd = true;
input.keyboard.xkb = {
layout = "us";
variant = "intl";
};
layout = {
gaps = 4;
focus-ring = {
enable = true;
width = 2;
active.color = "#${config.colorScheme.palette.base0D}ff";
inactive.color = "#${config.colorScheme.palette.base03}ff";
};
};
};
};
Color scheme with nix-colors
Instead of hardcoding hex values everywhere, I use nix-colors to apply a consistent color scheme across all apps. I pick a scheme once and reuse it everywhere.
imports = [ inputs.nix-colors.homeManagerModules.default ];
colorScheme = inputs.nix-colors.colorSchemes.dracula;
Then reference colors anywhere:
active.color = "#${config.colorScheme.palette.base0D}ff";
This works for Alacritty, Niri’s focus ring, tab indicators — anything that accepts hex colors.
Alacritty colors
Here’s an example applying the scheme to Alacritty:
programs.alacritty.settings.colors = {
primary = {
background = "#${config.colorScheme.palette.base00}";
foreground = "#${config.colorScheme.palette.base05}";
};
normal = {
black = "#${config.colorScheme.palette.base00}";
red = "#${config.colorScheme.palette.base08}";
green = "#${config.colorScheme.palette.base0B}";
yellow = "#${config.colorScheme.palette.base0A}";
blue = "#${config.colorScheme.palette.base0D}";
magenta = "#${config.colorScheme.palette.base0E}";
cyan = "#${config.colorScheme.palette.base0C}";
white = "#${config.colorScheme.palette.base05}";
};
};
Screenshot
The native Niri screenshot action works but requires the XDG portal to be running.
The bind:
"Print".action.screenshot = {};
Scratchpad terminal
Niri doesn’t have a built-in scratchpad like i3, but you can implement one with a shell script and Nirius. Press Super+S to toggle a floating Alacritty terminal. If it doesn’t exist yet, it creates one. If it’s visible, it hides it to a named workspace. If it’s hidden, it brings it back.
The key insight is using a named workspace (scratchpad-hidden) as a holding area and tracking the window by app-id via niri msg -j windows and jq.
The bind:
"Mod+S".action = spawn "/home/YOUR_USER/.local/bin/alacritty-scratchpad";
#!/usr/bin/env sh
set -eu
NIRIUS=$(which nirius)
NIRIUSD=$(which niriusd)
APP_RE='^scratchpad-alacritty$'
APP_ID='scratchpad-alacritty'
# Scratchpad commands require the daemon.
if ! pgrep -x niriusd >/dev/null 2>&1; then
"$NIRIUSD" >/dev/null 2>&1 &
sleep 0.1
fi
center_if_scratchpad_focused() {
if niri msg -j focused-window 2>/dev/null | rg -q "\"app_id\"\\s*:\\s*\"$APP_ID\""; then
niri msg action center-window >/dev/null 2>&1 || true
fi
}
# Toggle: if scratchpad window exists, this shows or hides it.
if "$NIRIUS" scratchpad-show -a "$APP_RE" >/dev/null 2>&1; then
center_if_scratchpad_focused
exit 0
fi
# First run: create the terminal with normal Alacritty decorations.
alacritty --class scratchpad-alacritty --title Scratchpad &
i=0
while [ "$i" -lt 60 ]; do
sleep 0.05
if "$NIRIUS" focus -a "$APP_RE" >/dev/null 2>&1; then
"$NIRIUS" scratchpad-toggle -a "$APP_RE" >/dev/null 2>&1 || true
"$NIRIUS" scratchpad-show -a "$APP_RE" >/dev/null 2>&1 || true
center_if_scratchpad_focused
exit 0
fi
i=$((i + 1))
done
exit 1
With this window rule:
{
matches = [{ app-id = "^scratchpad-alacritty$"; }];
open-floating = true;
default-column-width.proportion = 0.60;
default-window-height.proportion = 0.50;
}
Absolute paths
The Niri spawn path doesn’t inherit your shell’s
PATH, so always use absolute paths for scripts called from binds.
Vicinae script commands
Vicinae is the launcher I use. It supports script commands: shell scripts with a few metadata directives that get indexed and show up in the search.
The catch on NixOS is that Home Manager creates symlinks into the Nix store, and Vicinae doesn’t follow them when scanning script directories. The fix is mkOutOfStoreSymlink:
home.file.".local/share/vicinae/scripts".source =
config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/nixos/scripts/vicinae";
Scripts in that folder just need the right directives:
#!/usr/bin/env bash
# @vicinae.schemaVersion 1
# @vicinae.title Open NixOS config
# @vicinae.mode silent
subl ~/nixos
Reloading scripts
After adding scripts, search for “Reload Script Directories” inside Vicinae to pick them up without restarting.
Binary caches
Niri, Noctalia, and Vicinae aren’t in the main nixpkgs cache, so without binary caches you’ll be compiling them from source. Add their Cachix caches to avoid that:
nix.settings = {
substituters = [
"https://cache.nixos.org"
"https://niri.cachix.org"
"https://vicinae.cachix.org"
];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"niri.cachix.org-1:Wv0OmO7PsuocRKzfDoJ3mulSl7Z6oezYhGhR+3W2964="
"vicinae.cachix.org-1:1kDrfienkGHPYbkpNj1mWTr7Fm1+zcenzgTizIcI3oc="
];
};
Final thoughts
The part I care about most is that my desktop now lives in versioned files: packages, keybinds, colors, SSH hosts, and git config. A bad change is one rollback away, and a new machine starts from git clone plus nixos-rebuild switch.
Comments