- Lua 57.4%
- Shell 27.6%
- QML 10.4%
- Dockerfile 2%
- Python 1.4%
- Other 1.2%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| .githooks | ||
| image | ||
| packages | ||
| stow | ||
| .gitignore | ||
| AGENTS.md | ||
| bootstrap.sh | ||
| CLAUDE.md | ||
| README.md | ||
| secretspec.toml | ||
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:
- installs tracked packages that are missing (
pkg restore, skip with--no-pkgs) - stows every package into
$HOME(--restow, so links to deleted files are removed) - installs root-owned files from
stow/system/(see below) when they changed - regenerates niri keybinds and re-applies noctalia's theme templates
- runs
fisher updatewhenfish_pluginschanged - 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 withhyprctl reload. - niri includes the generated
~/.config/niri/binds.kdl. Thebindgen.pathuser unit regenerates it wheneverbinds.luachanges, and niri reloads it live. - Key names are canonical (
Super/Ctrl/Alt/Shift+ XKB keysym, plusWheelUp/Down/Left/RightandMouseLeft/Right/Middle).lib/keys.luatranslates them for each WM. - Actions (
focus.left,workspace.move, …) are mapped per WM inlib/actions/<wm>.lua. An action a WM doesn't support is skipped with a warning. bindgen --checkreports conflicts and skipped binds.bindgen --list niri|hyprlandprints 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.tomland 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 modeper file (defaultroot: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 tostow/user/fonts/; symlinks are followed).sessions: "Niri (uwsm-managed)" login entry. Hyprland uses the stockhyprland-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.