Broad Arrow Linux

Restricted · Flight Crew Only
Hint: the beret guy.

Broad Arrow Linux

Mission Control's Hyprland Distro

The complete cold-start restore procedure: from a blank machine to your exact CachyOS + Hyprland + Noctalia setup — binds, workspaces, theme, apps and all. Follow the stages in order. Every command is copy-pasteable.

base CachyOS compositor Hyprland (Lua) shell Noctalia v5 login shell fish dotfiles bare git repo
Mission status: go for launch
0 / 9 stages complete
Stage T-9 · The metal

Install CachyOS

  1. Download the latest CachyOS ISO from cachyos.org and flash it to a USB stick (Ventoy, or dd if you're feeling brave).
  2. Boot the new machine from USB (usually F2/F12/Esc at power-on for the boot menu; disable Secure Boot if it complains).
  3. In the CachyOS installer, when asked for a desktop, choose Hyprland — this pulls in the cachyos-hypr-noctalia dots that everything below assumes.
  4. Finish the install, reboot into the new system, connect to Wi-Fi, and open kitty.
  5. Full system update before anything else:
sudo pacman -Syu
iDo the Noctalia post-setup prompts if they appear (workspaces.lua save location etc.) — the dotfiles restore will overwrite most of it anyway, so don't agonise over choices here.
Stage T-8 · Comms

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
You want "Hi <username>! You've successfully authenticated". Note the username it greets you with — that's the account your dotfiles repo must live under.
iFirst connection will ask to trust GitHub's host key. The legit ED25519 fingerprint is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU — if it matches, type yes.
Stage T-7 · Payload

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 true
!Replace YOURUSER 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 -f deliberately overwrites the fresh install's default configs with yours — that's the point.
  • The alias -s saves permanently in fish, so dots works in every future terminal.
  • push.autoSetupRemote is 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.
Stage T-6 · Guidance calibration

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 whatever hyprctl monitors said).
  • Leave MONITOR2/MONITOR3 as "" 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.

Window moves throwing "invalid monitor / monitor doesn't exist"? It's always this stage. Re-check 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:

  1. Fill in MONITOR2 = "DP-3" (or whatever hyprctl monitors calls it) in variables.lua, and give it a block in monitors.lua with its mode/position/scale.
  2. 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.
  3. 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 devices for 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 })
!Why conditional matters: an unconditional "disable panel on lid close" bind black-screens the laptop when used undocked — lid close kills your only display, then suspend; on wake there are zero enabled monitors and the machine looks dead. Recovery without a hard reset: Ctrl+Alt+F3 for a TTY, log in, 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.
Safe test order: undocked with lid open, run ~/.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.
!The phantom-panel gotcha: without those binds, the laptop screen stays invisibly alive inside the closed lid — workspaces 1–5 live there, you get shunted onto workspace 6+, and absolute workspace binds appear to "do nothing" because they're switching a screen you can't see. Diagnosis in one command: 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.
iThese lid binds coexist with the logind lid config from Stage T-3: HandleLidSwitchDocked=ignore keeps the machine awake when docked, and the binds handle the display. Undocked, lid close still suspends as normal.
Stage T-5 · Tooling

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
Stage T-4 · Cargo

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
iThe AUR pass takes a while — it builds from source. If anything hardware-specific to the old machine errors (vendor tools, odd drivers), skip it rather than fight it: yay -S --needed pkg1 pkg2 ... with just the ones you care about works too.
iReminder of the two golden lookups when a keybind won't launch something: 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.
Stage T-3 · Ground systems

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.toml from etc-backup, then systemctl disable sddm && systemctl enable greetd). If you never left SDDM, skip.
Escape hatch, always: Ctrl+Alt+F3 for a TTY, log in, sudo systemctl disable greetd && sudo systemctl enable sddm, reboot.
Stage T-2 · Automation

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 dotsync

Then 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
If the push fails with "the current branch master has no upstream branch": the 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.)
iPlain 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.
Stage T-1 · Final checks

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-id targets your keybinds use. (--app=URL style 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)
All green? Touchdown. The machine is now a Broad Arrow Linux unit. Go build something.
Orbit · Ongoing operations

Mission ops — the regular habits

CommandWhat it doesWhen
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
Two-machine caveats: the package lists regenerate from whichever machine runs dotsync — if both laptops stay alive long-term, switch dotsync to per-hostname filenames (~/.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.)
iWhen CachyOS ships new dots: run dotsync first so your state is clean, then use 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.