No description
  • Lua 57.4%
  • Shell 27.6%
  • QML 10.4%
  • Dockerfile 2%
  • Python 1.4%
  • Other 1.2%
Find a file
Jo 4b7d52a390
All checks were successful
image / build (push) Successful in 2m0s
image: drop the SDDM drop-in
2026-10-24 17:05:31 +02:00
.forgejo/workflows image: rebuild only when bazzite:stable changes 2026-10-23 09:49:23 +02:00
.githooks hooks: skip file checkouts 2026-10-11 21:12:13 +02:00
image image: drop the SDDM drop-in 2026-10-24 17:05:31 +02:00
packages packages: markdownlint-cli instead of markdownlint in box-dev 2026-10-24 14:25:53 +02:00
stow pkg: export box package commands to ~/.local/bin 2026-10-24 14:25:53 +02:00
.gitignore monitors: per-machine profiles with Super+P overlay 2026-10-19 15:21:13 +02:00
AGENTS.md image: create /nix mount point for Nix 2026-10-24 17:05:27 +02:00
bootstrap.sh monitors: update script 2026-10-24 12:01:35 +02:00
CLAUDE.md docs: add AGENTS.md 2026-10-14 19:14:31 +02:00
README.md image: drop the SDDM drop-in 2026-10-24 17:05:31 +02:00
secretspec.toml Initial stow-based dotfiles 2026-10-11 21:11:49 +02:00

dotfiles

Managed with GNU stow. The repo root holds only the tooling; the configs live under stow/:

stow/user/<pkg>/     stow packages mirroring $HOME, e.g. stow/user/niri/.config/niri/config.kdl
stow/system/<pkg>/   root-owned files mirroring / (see System files)
packages/            package lists for pkg
image/               the Bazzite system image (see Bazzite image)
git clone <repo> ~/dotfiles && ~/dotfiles/bootstrap.sh

bootstrap.sh is the only command to remember. It is idempotent and only acts (or asks for sudo) when something is out of date. After the first run, git hooks (.githooks/) rerun it after every git pull / checkout. It:

  1. installs tracked packages that are missing (pkg restore, skip with --no-pkgs)
  2. stows every package into $HOME (--restow, so links to deleted files are removed)
  3. installs root-owned files from stow/system/ (see below) when they changed
  4. regenerates niri keybinds and re-applies noctalia's theme templates
  5. runs fisher update when fish_plugins changed
  6. enables user services (bindgen.path) and the git hooks

Generated files are gitignored.

Keybinds (Hyprland + niri)

All keybinds live in stow/user/keybinds/.config/keybinds/binds.lua:

bind({ keys = "Super+Return", app = "ghostty", desc = "Terminal" })            -- both WMs
bind({ keys = { default = "Super+B", hyprland = "Super+Tab" }, action = "overview" }) -- per-WM chord
bind({ only = { "hyprland" }, keys = "Super+Y", action = "window.pin" })      -- subset of WMs
bind({ only = { "niri" }, keys = "Super+C", raw = { niri = "center-column" } }) -- WM-native action
  • Hyprland loads it directly (stow/user/hypr/.config/hypr/config/keybinds.lua). Reload with hyprctl reload.
  • niri includes the generated ~/.config/niri/binds.kdl. The bindgen.path user unit regenerates it whenever binds.lua changes, and niri reloads it live.
  • Key names are canonical (Super/Ctrl/Alt/Shift + XKB keysym, plus WheelUp/Down/Left/Right and MouseLeft/Right/Middle). lib/keys.lua translates them for each WM.
  • Actions (focus.left, workspace.move, …) are mapped per WM in lib/actions/<wm>.lua. An action a WM doesn't support is skipped with a warning.
  • bindgen --check reports conflicts and skipped binds. bindgen --list niri|hyprland prints the cheat sheet.
  • Super+? (Super+Shift+ssharp, the ß key on the German layout) opens a searchable cheat sheet of the running compositor's binds in noctalia's launcher. Selecting an entry runs it.

Monitors (Super+P)

Each machine has stow/user/monitors/.config/monitors/<hostname>.json: its main monitor, per-monitor settings (outputs: mode, scale, transform, vrr, extra hyprland fields such as HDR) and an ordered list of profiles. A profile lists the monitors that are on (by make model serial as niri msg outputs shows it, or main) with their position/mirror (or "enabled": false), plus an icon (tabler glyph name); all other monitors are off. The key "*" stands for every other monitor, known or not, so whatever gets plugged in is used: with "side": "right" or "left" they line up next to main (in connector order, top-aligned). monitors watch (monitors-watch.service, udev DRM events) re-renders on every hotplug, and falls back to the first fitting profile while the chosen one doesn't fit (e.g. "External only" with nothing plugged in). The laptop (bazzite.json, the built-in panel as main) has "Extend right", "Extend left", "Laptop only", "Duplicate" (Hyprland) and "External only". Like Windows' Win+P, Super+P shows the profiles that fit the connected monitors as cards on every screen (a Quickshell overlay, ~/.config/monitors/overlay, colored by noctalia's theme), starting on the current profile. Pressing P again while holding Super moves the selection; releasing Super applies it (or Enter, or a click on a card). Esc closes the overlay without changing anything.

monitors renders the active profile into ~/.config/niri/outputs.kdl and ~/.config/hypr/monitors.lua (gitignored), so both compositors and the next login use it, then runs noctalia msg greeter-sync so the login screen gets the new layout too. With a single-output profile ("Main only", "Second only") the login screen is pinned to that output and the other one stays off (/usr/local/bin/greeter-output, from stow/system/greetd). monitors list|set NAME|save|init: save stores the live positions/modes into the active profile, and init creates the file for a new machine from its live layout.

Shell and colors

noctalia v5 is the desktop shell on niri and Hyprland: bar, launcher (Super+Space), clipboard history (Super+V), notifications, OSD, lock screen, idle and wallpaper. Its config is stow/user/noctalia/.config/noctalia/*.toml. Changes made in its settings UI land in ~/.local/state/noctalia/settings.toml (untracked) and override the tracked files.

Colors come from the wallpaper. noctalia renders the app templates in stow/user/noctalia/.config/noctalia/templates/ (niri, Hyprland, ghostty, GTK, Qt, btop, cava, zathura, fuzzel, yazi; matugen-compatible syntax), as declared in theme.toml.

wallpaper IMAGE sets the wallpaper through noctalia. Animated images (GIF) play via the noctalia/mpvpaper plugin; one complete frame becomes noctalia's wallpaper so the colors follow. wallpaper alone re-applies the last. Under Plasma (no noctalia) it sets Plasma's wallpaper instead (that frame for GIFs) and renders the same templates with matugen, leaving out GTK and Qt, which Plasma themes.

Super+Shift+W (wallpicker) opens a wallpaper picker: a Quickshell layer-shell overlay on the focused output, so it looks the same under niri and Hyprland. It lists ~/Pictures/wallpapers recursively, is themed by noctalia's colors, and runs wallpaper IMAGE on Enter or double-click. Thumbnails are cached in ~/.cache/wallpaper/thumbs.

Fonts

Fonts are chosen once, in stow/user/fonts/.config/fontconfig/fonts.conf: sans-serif = Ubuntu Nerd Font, monospace = BlexMono Nerd Font Mono (IBM Plex Mono), serif = Noto Serif. Apps only ask for these generic names (noctalia, GTK, Qt, ghostty, kitty, fuzzel, Hyprland), so changing a font is a one-line edit. The same file is installed system-wide as /etc/fonts/local.conf (stow/system/fonts). Check with fc-match sans-serif / fc-match monospace.

Default apps

Opening files and links goes through handlr-regex (config in stow/user/handlr/). The default apps that matter are listed in ~/.config/handlr/defaults (MIME DESKTOP-FILE per line); bootstrap applies them with handlr set. ~/.config/mimeapps.list itself is not tracked, because apps rewrite it and register their own URL handlers there. Bootstrap remembers the defaults as they were after its last run (~/.local/state/dotfiles/default-apps). If one changed on the system since then ("always open with", handlr set, a newly registered URL handler), it asks whether to adopt it into defaults or, for tracked types, reset it to the repo value. Without a terminal (e.g. from a git hook) it only warns and asks on the next interactive run.

Packages

pkg (in stow/user/bin/) wraps package managers and records what you install in packages/<manager>.txt. The manager is recorded too, because package names differ between distros.

pkg add ripgrep                      # default manager (paru on Arch)
pkg add -m flatpak -r "notes" com.github.flxzt.rnote
pkg rm ripgrep
pkg diff                             # installed but untracked / tracked but missing
pkg restore                          # install everything tracked for the available managers

Distroboxes are declared in packages/distrobox.ini (distrobox assemble format; the dev box is Arch with paru). bootstrap creates each box, recreates it when its section changes ($HOME is shared and its packages are reinstalled right after) and only reports boxes that left the file. Each box is a manager box-NAME with its list in packages/box-NAME.txt:

pkg add -m box-dev ripgrep           # on the host: runs paru through `distrobox enter dev --`
pkg add ripgrep                      # inside the dev box: box-dev is the default there

Inside a box the host's paru/yay/pacman lists are unavailable, so nothing lands in paru.txt by accident. The commands of a box's tracked packages (their /usr/bin files) are exported to ~/.local/bin as distrobox-export wrappers, so they run from the host. pkg add, pkg rm and pkg restore (and with it bootstrap) keep the exports in sync with the list; existing files in ~/.local/bin are never replaced.

Secrets

Nothing secret is tracked.

  • SSH keys: kept in Bitwarden and served by the Bitwarden desktop SSH agent (~/.bitwarden-ssh-agent.sock). The agent also signs commits.
  • Env secrets: declared in secretspec.toml and resolved from Bitwarden through secretspec (bw:// provider): secretspec run -- <cmd>.
  • Machine-local git settings can go in the untracked ~/.config/git/private.inc (included if it exists).

System files

stow/system/<package>/ mirrors / for root-owned files. They are copied, never linked: root config must not point into a user-writable home, and system users such as greeter can't read it. Bootstrap installs a file only when it changed (recorded in ~/.local/state/dotfiles/system-files) and removes files deleted from the repo. Optional per package:

  • .attrs: path owner:group mode per file (default root:root 0644)
  • .requires: commands or paths that must exist, otherwise the package is skipped
  • .post-install: run with sudo after any of the package's files changed

Packages:

  • fonts: the central fontconfig as /etc/fonts/local.conf (a symlink to stow/user/fonts/; symlinks are followed).
  • sessions: "Niri (uwsm-managed)" login entry. Hyprland uses the stock hyprland-uwsm.desktop.
  • greetd: greetd runs noctalia-greeter, configured in /var/lib/noctalia-greeter/greeter.toml. noctalia syncs its theme and wallpaper to it. Fallback from a TTY: sudo cp /etc/greetd/config.toml.tuigreet /etc/greetd/config.toml && sudo systemctl restart greetd.
  • gamermode: polkit rule letting the noctalia gamer-mode plugin stop/start its fixed list of system units without a password.
  • finalmouse: udev rule giving the logged-in user access to Finalmouse hidraw devices, so XPanel can find the mouse.

Environment variables go in stow/user/uwsm/.config/uwsm/env (shared), env-hyprland and env-niri.

Bazzite image

The laptop (ROG Zephyrus G14) runs Bazzite, where /usr is read-only. Its system packages come from an image: image/Containerfile builds on ghcr.io/ublue-os/bazzite:stable and adds the packages that used to be layered with rpm-ostree (asusctl, supergfxctl, zerotier-one, powertop) plus what the dotfiles need (stow, neovim, ghostty and Zen from COPR, the Ubuntu and BlexMono Nerd Fonts; fish is in the base), and niri, Hyprland (nett00n/hyprland COPR), uwsm, noctalia, quickshell, mpvpaper and matugen for a second desktop next to Plasma. It also creates the /nix mount point for Nix (Determinate installer), which can't be created on the read-only root at runtime. Add system packages there, not with pkg. image/files/ mirrors / (e.g. the uwsm session entries for niri and Hyprland, and a Plasma Login Manager drop-in that preselects jk with Plasma).

.forgejo/workflows/image.yml builds it on every change under image/, and when Bazzite releases: every 3 hours it compares the digest of bazzite:stable with the base recorded in the image's org.opencontainers.image.base.name label and skips the build if they match. It pushes git.joachimkern.de/charging1948/bazzite:latest plus a date tag. It needs the repository secret REGISTRY_TOKEN: a Forgejo access token with write:package.

Switching the machine to the image, once (the layered packages are in the image now):

sudo rpm-ostree reset --overlays
sudo rpm-ostree rebase ostree-unverified-registry:git.joachimkern.de/charging1948/bazzite:latest
systemctl reboot

After that, normal Bazzite updates pull the image. The previous deployment stays in the boot menu; sudo rpm-ostree rollback goes back to it.

On Bazzite, bootstrap installs nothing from stow/system/ (root files belong in image/files/) and removes what an earlier run installed there; pkg never picks dnf, and noctalia does not start under Plasma. Userland tools go into the dev distrobox (see "Packages") or flatpak.