Rebuilding My Arch Setup

My CachyOS, Umbriel and Noctalia setup in a chezmoi-managed dotfiles repo, validated in CI, and a detour into NixOS.

I rebuild my Linux setup more often than I need to. Not because something broke, usually, but because a reinstall is the only honest test of whether your configuration is actually reproducible. If getting back to a working desktop takes an afternoon of remembering, it was never configuration; it was just accumulated state.

So the goal for this iteration was simple: a fresh machine should reach my full environment from a single command, and everything that could silently rot should be checked by something other than me.

The stack

LayerTool
OSCachyOS (Arch-based)
CompositorUmbriel, Niri as fallback
Desktop shellNoctalia (via Quickshell)
TerminalAlacritty
ShellFish
EditorNeovim (LazyVim), Zed
Dotfileschezmoi

CachyOS gives me Arch with sane defaults and a working installer, which removes the part of the Arch experience I have no interest in repeating. Umbriel is the compositor I run day to day, with Niri still installed as a fallback, and Noctalia is the shell on top of either. Niri comes first below because it shaped how I work, followed by why I moved to Umbriel. Noctalia also generates a matugen palette that my Neovim colorscheme picks up, so the editor follows the wallpaper.

Three Niri columns on an ultrawide monitor: Neovim with the LazyVim dashboard, fastfetch above btop in Alacritty, and a terminal radio player

That is the whole desktop on my ultrawide: Neovim, a terminal with fastfetch and btop, and a radio player, laid out as columns on Niri's strip with Noctalia's bar across the top. The screenshot is generated by the repo itself (scripts/screenshot.sh opens a demo workspace and captures it), so it can't drift from what the config actually produces. The script still drives Niri's IPC, so it shows the Niri session.

Starting with Niri

Most tiling window managers pack windows into a grid that has to fit the screen. Open a fourth window and every other window shrinks to make room. Niri drops that constraint: windows sit in columns on an infinite horizontal strip, and the screen is a viewport onto it.

What that buys over a traditional tiler like i3 or Hyprland's default dwindle layout:

  • Nothing resizes behind your back. A new window opens next to the focused one and scrolls into view; the windows you already had keep their size. Closing one doesn't reflow the rest either.
  • It copes with screen size. On a laptop panel, five windows in a grid are five unreadable slivers. On a strip, they are five readable windows and you scroll between them. On my ultrawide it is the opposite: three columns fit comfortably, and a fourth just continues off the edge.
  • Workspaces are dynamic and per monitor. They form a vertical stack that grows as needed, so I never decide in advance that "workspace 7 is for chat".
  • Columns are the unit. Stacking a window under another, tabbing a column, or switching a column between a fixed and a proportional width are all small moves, not a relayout of a tree.
  • The config is plain KDL, and it can be validated. niri validate checks a file without a session, which is what makes the CI job below possible. The config also reloads live, so I can adjust window rules without logging out.

The cost is that it is a Wayland-only compositor in a younger ecosystem. Some things that X11 tilers got for free (global hotkey tools, certain screen-capture setups) need a portal or a Wayland-native replacement.

Moving to Umbriel

Since mid-October the repo also carries Umbriel, a Wayland tiling compositor from the Noctalia project. I first added it as an alternative to pick at the login screen. It has since become the compositor I run every day, and Niri stays installed as a fallback. It keeps the scrolling column layout I got used to on Niri, and a few things made it the better fit:

  • It fits Noctalia better. Both come from the same project, and it shows. Umbriel starts Noctalia itself, its keybinds for the launcher, control center, lock screen and volume are plain noctalia msg calls, and its config pulls in an optional noctalia.toml next to my own files. The shell feels like part of the compositor rather than something sitting on top of it.
  • Minimal, but nothing is missing. It is small and lightweight, and still covers everything I used Niri for: scrolling columns, window and layer rules, per-output config and a keybind overlay.
  • Streaming has a proper recipe. Sunshine does not work without changes, but Umbriel's game streaming guide covers the whole setup: Sunshine captures through wlr-screencopy, and a small prep script creates a virtual sunshine output at the client's resolution for each session and removes it afterwards, so my ultrawide monitor is capable of changing to cropped 16:9 and mirror it's screen to my OLED TV. Window rules send Steam Big Picture and Proton games to that output.
  • Better multi-monitor handling. Layout settings can be set per output, including the orientation. On my desktop the 1440p monitor next to the ultrawide is rotated into portrait, so that output gets its own workspace axis and new windows open at half its extent, while the ultrawide keeps the defaults:
cfg/outputs.toml.tmpl
[output.DP-2]
mode = "[email protected]"
transform = "90"
position = [3440, -420]
workspace_axis = "horizontal"
layout.scrolling.default_extent_fraction = 0.5

Umbriel is still at an early stage (the package is a 0.1 development snapshot), so I expect to hit rough edges. I am fine with that: I like the effort that is going into it, and that is a big part of why I switched. The documentation is already thorough, too: layouts, outputs, window rules and streaming all have their own pages with working examples.

Its config follows the same pattern as Niri's: a config.toml plus split cfg/*.toml files, where only outputs, input and window rules are templated per machine. The keybind overlay is on Mod+Shift+Esc in both, so falling back to Niri does not mean relearning the basics. Both configs are held to the same bar as everything else: they have to validate in CI.

Why Noctalia

A bare compositor leaves you assembling a desktop: a bar, a launcher, a notification daemon, a wallpaper tool, a lock screen, a control panel, each with its own config format and its own theming story. Noctalia is a shell built on Quickshell that ships those as one coherent unit, so they share a design and a single settings surface.

The advantage over the usual bar-plus-launcher-plus-daemon stack is integration:

  • One thing to configure and theme, instead of five configs that each need the same colors.
  • A wallpaper-driven palette. It generates colors from the current wallpaper and exposes them through templates, which is how Neovim ends up matching the desktop.
  • Per-machine overrides stay small. Its config is data, so the parts that differ between my machines are a few templated values rather than a forked setup.

It is also a single moving part: when something looks wrong, there is one place to look.

Why not a bare repo

The first version of this setup was a bare git repo with $HOME as its work tree. It is a lovely trick and it works for a single machine. It stops working the moment a file needs to differ between two machines, or contain something you would rather not publish: there is no templating, so the answer is a branch per machine, and a branch per machine is a fork you maintain by hand.

I moved to chezmoi. The repo is a source tree (.chezmoiroot points it at home/), and chezmoi apply renders it into $HOME. The day-to-day loop is still close to plain git:

chezmoi edit ~/.config/niri/cfg/binds.kdl
chezmoi diff
chezmoi apply
chezmoi update   # pull, apply, install any new packages

What I get in exchange for the extra indirection:

  • Templates. Files ending in .tmpl are rendered per machine, so one source file can produce different output for different hosts.
  • Prompts on first run. My git name and email are asked for once instead of being hardcoded into a public repo.
  • A diff before every apply. The state in $HOME and the state in the repo are two different things, and chezmoi shows the gap rather than hiding it.

One command to bootstrap

On a fresh machine, the chezmoi installer fetches the binary and applies the repo in one go:

sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply [email protected]:robinsmith-source/dotfiles.git

The first apply does the rest. It installs the Bitwarden CLI if it is missing, then offers to install every package listed in .chezmoidata/packages.yaml. A run-script hook reruns that whenever the file changes, so adding a package to the list is also how I install it. Packages are grouped by where they come from: pacman, pacman_cachyos (only installed when /etc/os-release says CachyOS, which is where Umbriel comes from), pacman_laptop, and aur.

Afterwards it installs the repo's pre-commit hooks and validates the Niri, Umbriel and Noctalia configs, so a broken config is reported at apply time rather than at the next login.

Secrets in a public repo

The repo is public and contains no secrets. Anything sensitive is a .tmpl file that pulls from Bitwarden when it is applied. SSH keys are served by the Bitwarden SSH agent and never land on disk, and git signs my commits over SSH through that same agent.

As a backstop, gitleaks runs as a pre-commit hook and again in CI over the full history, next to trufflehog. The point is not that I expect to leak something; it is that "I would notice" is not a control.

One repo, two machines

Host-specific values now live in the templates instead of in a branch. A .laptop flag is true when the hostname is goblin, and it switches:

  • the monitor layout and input config for Niri and Umbriel (both are split into cfg/* files, so only the display and input parts are templated)
  • the keyboard layout to de,us
  • some focus behaviour
  • extra packages like upower and power-profiles-daemon

This was the main gap in the previous version of this post, and it is the reason for the whole migration.

Letting CI catch the rot

This is the part I would actually recommend to anyone keeping dotfiles in git. Config files fail at the worst possible time: after a reinstall, when you have no working environment to debug in. So the repo validates itself on every push.

The generic checks come from pre-commit and run both locally and in CI: shellcheck and shfmt for scripts, stylua for the Neovim config, and syntax checks for YAML, TOML and JSON. Renovate keeps the hook versions and GitHub Actions up to date weekly.

The interesting job runs inside an archlinux container and again in a cachyos one, and does a clean chezmoi init --apply. That one step covers a lot:

  • the templates have to render with the default answers to the prompts and no secrets available
  • niri validate parses the compositor config and fails on anything it cannot understand
  • the Noctalia config is validated the same way, and umbriel config validate checks the Umbriel config in the CachyOS run, since Umbriel is only packaged there
  • fish --no-execute does a syntax-only pass over each fish file
  • every package in the pacman lists has to actually exist in the repos (the CachyOS-only and AUR lists can't be resolved in that container)

Neither Niri nor fish needs a display or a session for this, so a change that would have left me at a black screen after the next reboot, or a package that was renamed upstream, fails in a pull request instead.

The NixOS detour

Reproducibility has a more thorough answer than "a script that installs things": declare the whole system. I tried it in a separate flake repo for the same two machines, a Ryzen laptop and an AMD desktop, both running Niri and Noctalia.

The structure is pleasant. Shared NixOS modules expose workstation.* enable options, a mkWorkstation helper builds each host from a device module plus a list of Home Manager imports, and a per-host import list means something like cava can exist on the desktop only. Installing is nixos-install --flake and rolling back is nixos-rebuild switch --rollback. That is a better reproducibility story than mine, because the system itself is the artifact and not just the dotfiles.

I have not made it my daily driver. The repo is still labelled as a testing ground, and the reason is mostly cost: every tool that ships its own config or updater (Neovim plugins, Noctalia's templates) needs a decision about who owns the file, Nix or the tool. On Arch I let the tool own it and chezmoi tracks the result. Nix is the more correct answer, and chezmoi is the one I can keep up with. For now the flake is where I learn, and the dotfiles repo is where I live.

What I would still change

Package installation is declarative only in one direction: a package added to packages.yaml gets installed, but one removed from it stays on the machine. Nothing prunes drift, so packages.yaml describes a lower bound of the system rather than the system. Closing that gap, by diffing installed explicit packages against the list in CI or in update, is the next thing worth doing; it is also the part where Nix is simply ahead.

Last updated: Nov 11, 2026