Install CachyOS
- Download the latest CachyOS ISO from cachyos.org and flash it to a USB stick (Ventoy, or
ddif you're feeling brave). - Boot the new machine from USB (usually F2/F12/Esc at power-on for the boot menu; disable Secure Boot if it complains).
- In the CachyOS installer, when asked for a desktop, choose Hyprland — this pulls in the
cachyos-hypr-noctaliadots that everything below assumes. - Finish the install, reboot into the new system, connect to Wi-Fi, and open kitty.
- Full system update before anything else:
sudo pacman -Syu
Connect to GitHub
New machine, new SSH key. Generate one and hand the public half to GitHub.
ssh-keygen -t ed25519 -C "eddie@$(hostname)" cat ~/.ssh/id_ed25519.pub
Copy the whole ssh-ed25519 ... line, then on github.com: Settings → SSH and GPG keys → New SSH key, paste, name it after the machine.
Verify the handshake:
ssh -T git@github.com
SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU — if it matches, type yes.Deploy the dotfiles
Clone the bare repo, set up the dots alias, and check your config out over the stock dots.
git clone --bare git@github.com:YOURUSER/dotfiles.git $HOME/.dotfiles
alias -s dots '/usr/bin/git --git-dir=$HOME/.dotfiles --work-tree=$HOME'
dots checkout -f
dots config status.showUntrackedFiles no
dots config push.autoSetupRemote trueYOURUSER with your actual GitHub username. Yes, really. We have now pushed to the literal user "YOURUSER" on two separate occasions. Do not make it three. ssh -T git@github.com tells you the right name if in doubt.checkout -fdeliberately overwrites the fresh install's default configs with yours — that's the point.- The
alias -ssaves permanently in fish, sodotsworks in every future terminal. push.autoSetupRemoteis the important one for later: a fresh clone's first push otherwise fails with "the current branch master has no upstream branch" — this makes first pushes (and therefore dotsync) just work on every new machine.
Fix the hardware variables
Different laptop = possibly different display name. Do this before logging out, or the workspace rules point at a ghost monitor (you know the error notification by heart).
hyprctl monitors
Note the output name (e.g. eDP-1). Then:
nano ~/.config/hypr/config/variables.lua
- Set
MONITOR1 = "eDP-1"(or whateverhyprctl monitorssaid). - Leave
MONITOR2/MONITOR3as""until real screens exist for them.
Also glance at ~/.config/hypr/config/monitors.lua — a different panel may want a different mode and scale than the old machine (check resolution options in the hyprctl monitors output).
Now log out and back in. This is the moment: your binds, workspaces, resize submaps, UK keyboard, natural scrolling, bar and theme should all appear.
variables.lua against hyprctl monitors, and make sure no binds reference an empty MONITOR2/MONITOR3.Docking & clamshell mode (lid closed, external monitor as the only screen) — when a big screen enters the picture:
- Fill in
MONITOR2 = "DP-3"(or whateverhyprctl monitorscalls it) in variables.lua, and give it a block in monitors.lua with its mode/position/scale. - Don't assign it workspaces if it's a clamshell-only screen — workspaces 1–5 migrate to it automatically once the internal panel is off. Per-monitor workspace splits are for genuinely dual-screen desks.
- Add conditional lid handling so the internal panel turns off when the lid closes — but only when docked. Two scripts plus two binds (check
hyprctl devicesfor the switch name; mirror the lid-open values from your monitors.lua MONITOR1 block):
# ~/.local/bin/lid-close (chmod +x) #!/bin/sh # Disable internal panel ONLY when the external monitor is connected if hyprctl monitors | grep -q "DP-3"; then hyprctl eval 'hl.monitor({ output = "eDP-1", disabled = true })' fi
# ~/.local/bin/lid-open (chmod +x) #!/bin/sh # Always restore the internal panel on lid open (harmless if already on) hyprctl eval 'hl.monitor({ output = "eDP-1", mode = "preferred", position = "auto-right", scale = 1.33 })'
-- binds.lua — Clamshell: conditional panel kill/restore
hl.bind("switch:on:Lid Switch", hl.dsp.exec_cmd("~/.local/bin/lid-close"), { locked = true })
hl.bind("switch:off:Lid Switch", hl.dsp.exec_cmd("~/.local/bin/lid-open"), { locked = true })reboot — or resurrect in place with hyprctl -i 0 eval 'hl.monitor({ output = "eDP-1", disabled = false })'. The DP-3 check in lid-close prevents the whole failure mode.~/.local/bin/lid-close by hand — screen must stay on (no DP-3 → no-op). Then a real undocked close/open cycle. Then a docked cycle for clamshell. And track the scripts: dots add ~/.local/bin/lid-close ~/.local/bin/lid-open.hyprctl activeworkspace — if it names eDP-1 while you're staring at the external, that's it. One-off fix for a lid that's already closed: hyprctl eval 'hl.monitor({ output = "eDP-1", disabled = true })' — then watch the workspaces flow home.HandleLidSwitchDocked=ignore keeps the machine awake when docked, and the binds handle the display. Undocked, lid close still suspends as normal.Install yay (AUR helper)
CachyOS doesn't ship an AUR helper by default. Build yay once, use it forever:
sudo pacman -S --needed base-devel git git clone https://aur.archlinux.org/yay.git /tmp/yay cd /tmp/yay makepkg -si cd ~
Sanity check:
yay --version
Restore all packages
The dotfiles brought ~/.pkglist.txt (everything explicitly installed) and ~/.aurlist.txt (the AUR subset). Repo packages first — this filters the AUR names out so pacman doesn't choke (fish syntax):
sudo pacman -S --needed - < (grep -vxFf ~/.aurlist.txt ~/.pkglist.txt | psub)
Then the AUR ones (Sublime, Slack, and friends) with yay:
yay -S --needed - < ~/.aurlist.txt
yay -S --needed pkg1 pkg2 ... with just the ones you care about works too.pacman -Ql <package> | grep /usr/bin/ tells you the real binary name (it's subl, not sublime-text-4; slack, not slack-desktop), and grep Exec /usr/share/applications/<app>.desktop shows exactly what the launcher runs.System files, lid & lock
The dotfiles repo can't touch /etc — restore those from your tracked ~/etc-backup/ folder:
sudo cp ~/etc-backup/logind.conf /etc/systemd/logind.conf sudo systemctl restart systemd-logind
That's the lid closes → suspend behaviour. Then re-check the lock half in Noctalia: Super + Z → Lock Screen → Behaviour → Lock on suspend (the setting travels with your Noctalia config, but verify — one lid-close test tells you everything).
- If the old machine ran hypridle:
systemctl --user enable --now hypridle.service - If you'd switched login to noctalia-greeter/greetd: that's a re-install on this machine (greeter package, setup script,
/etc/greetd/config.tomlfrom etc-backup, thensystemctl disable sddm && systemctl enable greetd). If you never left SDDM, skip.
sudo systemctl disable greetd && sudo systemctl enable sddm, reboot.Restore dotsync
Pull first — the function lives in the repo now, so on any machine set up after July 2026 it should simply arrive:
dots pull type dotsync
If fish prints the function — done, skip to the test below. If "not found", paste this whole block into the terminal and press Enter (order matters: funcsave on the last line is what creates the file on disk — you can't dots add it before this exists):
function dotsync -d "Commit and push dotfile changes"
pacman -Qqe > ~/.pkglist.txt
pacman -Qqem > ~/.aurlist.txt
dots add -u
if test (count $argv) -gt 0
dots commit -m "$argv"
else
dots commit -m "sync: "(date '+%Y-%m-%d %H:%M')
end
dots push
end
funcsave dotsyncThen make sure the function itself is tracked, so no future machine has to do this step:
dots add ~/.config/fish/functions/dotsync.fish
Either way, finish with a live-fire test — this proves the whole chain (function → commit → push) on this machine:
dotsync first sync from new machine
push.autoSetupRemote line from Stage T-7 got skipped. Fix both at once: dots push -u origin master then dots config push.autoSetupRemote true. (Your commit wasn't lost — it was sitting there waiting; the push just didn't know its destination yet.)dotsync with no message works fine too — words after it are just the commit message. Real messages make dots log readable; the sync itself is identical either way.Loose ends & the victory lap
- Wallpapers — the Noctalia setting travelled, the image files didn't. Copy them into the folder Noctalia expects (check Settings → Wallpaper for the path), then re-pick via the wallpaper panel. Palette-from-wallpaper re-themes everything automatically.
- Chrome — sign in; synced PWAs restore the
--app-idtargets your keybinds use. (--app=URLstyle binds just work with no sign-in.) - Slack / Sublime — sign in / add licence.
- Ark — should be back via the package list; right-click a zip in Dolphin to confirm Extract entries.
Then fly the test circuit — thirty seconds, touches everything we ever debugged:
- Super+2 switches workspace; Super+Shift+4 moves a window there — no red error notification
- Super+←/→ combo resize binds / submap behave
- Shift+3 types £ (kb_layout gb)
- two-finger scroll natural scrolling on the touchpad
- Slack keybind launches and tiles (and isn't fighting the scratchpad key)
- close the lid undocked: suspends → reopen → lock screen, display comes back (conditional lid scripts doing their job)
- docked + lid closed internal panel dies, workspaces consolidate onto the external — and Meta+1 goes somewhere you can see
- dotsync test commits & pushes clean, no upstream complaints (proven in T-2, but fly it again)
Mission ops — the regular habits
| Command | What it does | When |
|---|---|---|
dotsync |
The one-word backup: refreshes package lists, stages changes to tracked files, commits, pushes. Add a message when it matters: dotsync moved slack bind. |
After any config tweak you'd be sad to lose |
dots pull |
Fetches changes made on the other machine. If a push is ever rejected, this is why — pull, then push. | When you sit down at a machine you haven't used in a while |
dots push |
Push commits without the add/commit steps (dotsync does this for you; handy after a manual commit). | As needed |
dots add <path> |
Start tracking a new file. dotsync only sweeps files git already knows — new configs need this once. | First time you customise a new app |
dots status / dots log --oneline |
What's uncommitted / the history. dots diff answers "what changed since it last worked?" — the question that would have saved an hour of monitor-variable archaeology. |
Whenever something mysteriously breaks |
sudo cp ... ~/etc-backup/ |
The dotfiles repo can't see /etc. Copy system files into ~/etc-backup/ after changing them, then dotsync. |
Rare — logind/greetd changes only |
~/.pkglist-(hostname).txt). Same story for variables.lua if the monitor names differ: re-fix after pulling, or stop tracking it. (Or graduate to chezmoi and template it properly.)meld to merge their updated files into yours (the wiki's v4→v5 style workflow). Clean repo = you can always see what's yours vs theirs.