CachyOS Hyprland Developer Workstation Setup¶
Work in progress.
This document tracks a migration from Windows to a native Linux development workstation. It is updated as the setup is tested and refined.
- CachyOS Hyprland Developer Workstation Setup
- Goal
- Installation
- Packages
- Desktop
- Shell
- Theming
- Gaming
- Testing this project in a VM
- Apps shortcuts
Goal¶
Replace a Windows 11 + WSL workflow with a fully native Linux environment.
| Before | After |
|---|---|
| Windows 11 | CachyOS |
| WSL | Native Linux development |
| GlazeWM | Hyprland (no KDE Plasma desktop) |
| ShareX | grim + slurp + satty |
| Raycast | Vicinae |
| — | Noctalia (bar / control centre) |
| — | Ghostty (terminal) |
| — | Native Linux gaming |
KDE applications (Dolphin, Spectacle) are used where they are the best option.
Installation¶
Secure Boot¶
Disabled — the CachyOS installer was blocked by Secure Boot.
Bootloader and filesystem¶
- Bootloader: Limine
- Filesystem: Btrfs
- Encryption: LUKS
Keyboard layout defaults to QWERTY on first login¶
Even if you selected AZERTY during installation, Hyprland's login/greeter session
(noctalia-greeter, run via greetd — already the default for this Hyprland+Noctalia
setup) can still start in QWERTY. The layout lives in
/var/lib/noctalia-greeter/greeter.toml:
This file is root-owned and lives outside chezmoi's $HOME scope, so it isn't managed
by this repo yet. This command backs up the original, then upserts the [keyboard]
section (replacing it if it already exists, appending it if not) without touching the
rest of the file:
sudo cp /var/lib/noctalia-greeter/greeter.toml /var/lib/noctalia-greeter/greeter.toml.bak 2>/dev/null
sudo awk '
/^\[keyboard\]/ { print; print "layout = \"fr,us\""; print "options = \"grp:alt_shift_toggle\""; in_kb=1; done=1; next }
/^\[/ && in_kb { in_kb=0 }
in_kb && /^(layout|options)[[:space:]]*=/ { next }
{ print }
END { if (!done) print "\n[keyboard]\nlayout = \"fr,us\"\noptions = \"grp:alt_shift_toggle\"" }
' /var/lib/noctalia-greeter/greeter.toml | sudo tee /var/lib/noctalia-greeter/greeter.toml.new >/dev/null
sudo mv /var/lib/noctalia-greeter/greeter.toml.new /var/lib/noctalia-greeter/greeter.toml
sudo systemctl restart greetd
There is no on-screen button/icon in the greeter to switch layouts — per the
Noctalia greeter docs, switching only works
through the XKB keybind set in options above (Alt+Shift here).
Until [keyboard] is set, type your password using the QWERTY layout to log in,
then:
- Log in (QWERTY password entry).
- Install this app and sync the hyprland folder (already set to
"fr,us"in this project's config). - Reboot so the new layout takes effect everywhere, including the login screen.
- Install a browser — this project's
packages.yamlalready listshelium-browser-binandbrave-binundercachyos.pacman.Browsers.
After first boot¶
Verify the NVIDIA driver:
If this prints GPU information, the driver is working. CachyOS ships the NVIDIA stack preinstalled and keeps the kernel module in sync with the running kernel, so there is normally nothing to install.
You can also click the temperature readout in the Noctalia top bar to see CPU temperature, GPU name and general system information without opening settings.
Packages¶
Most of the packages below are managed by this project — run the app and install them from the UI rather than by hand:
The catalog lives in src/personal_os_setup/config/packages.yaml, under a single
cachyos: key. This project targets CachyOS specifically — generic Arch and
other derivatives are not supported.
Two package managers are used:
- pacman — official repositories
- paru — AUR. CachyOS ships paru by default;
Vicinae has its own installer:
Desktop¶
Configuration files¶
Copy the personal configuration for Hyprland and Noctalia into ~/.config.
Enable View → Show Hidden Files in Dolphin to see the directory.
Verify the keybindings afterward — for example ALT+F for fullscreen.
Settings owned by Noctalia, not Hyprland¶
Some desktop behavior that most Hyprland setups configure in hyprland.conf (or via
standalone daemons like hypridle/hyprlock/hyprpaper) is instead configured in
Noctalia's own ~/.config/noctalia/config.toml.
- Idle timeout / screen lock / suspend —
[idle.behavior.*]. Nohypridle/hyprlockinstalled or needed. - Wallpaper —
[theme] source = "wallpaper"plus thenzlov/daily-wallpaperplugin under[plugins]. Noswww/hyprpaper. - Night light / blue light filter —
[nightlight]. Notewlsunsetis still inpackages.yamltoo — check which one is actually active before assuming both run. - Screenshot pipeline —
[shell.screenshot]routes through Noctalia's own screenshot action (piped tosatty). - Session menu (lock / logout / reboot / shutdown / suspend) —
[[shell.session.actions]], each with its own shortcut inside the session menu.
Editing the lockscreen layout¶
The lockscreen widget layout is edited live, not through a config file: click the lock
icon (lockscreen-edit in the bar's start list, top-left of the Noctalia bar) to
toggle edit mode. Switch to an empty workspace first — otherwise windows behind the
lockscreen preview get in the way of seeing the live editing.
Hyprland plugins¶
hyprpm builds plugins from source, so the build tooling above must be
installed first.
hyprpm update
# ALT+TAB window switcher
hyprpm add https://github.com/gfhdhytghd/hymission
hyprpm enable hymission
# Title bars: drag, close, maximise
hyprpm add https://github.com/hyprwm/hyprland-plugins
hyprpm enable hyprbars
hyprpm reload
hyprpm list
Test ALT+TAB for the switcher, and dragging / closing / maximizing windows for hyprbars.
Secret storage (gnome-keyring, not KWallet)¶
Hyprland has no built-in Secret Service — anything that stores secrets (Nautilus/GVfs unlocking a
LUKS drive, browsers saving passwords, etc.) talks to org.freedesktop.secrets over D-Bus, and
nothing provides that name unless something starts a daemon for it. KDE's kwallet isn't advised to be used without plasma.
gnome-keyring is used instead — password-based, no GPG key needed, and the standard choice on non-KDE Wayland compositors:
- Not auto-unlocked with the login password, by choice. pam_gnome_keyring.so can do this, but it doesn't look at "whichever keyring is currently set as default"
- Apps using libsecret (Nautilus/GVfs, GTK/GNOME apps, Chromium-based browsers) otherwise just work — the manual prompt above is the only friction point.
- To view/delete stored secrets: sudo pacman -S seahorse ("Passwords and Keys" GUI), or secret-tool for CLI lookups.
- Bitwarden (website/app logins, secure notes, cards) is unrelated and doesn't cover this — it doesn't integrate with the system Secret Service, so it can't store or auto-unlock OS-level secrets like this LUKS passphrase, SSH SFTP, or NetworkManager WiFi keys.
Troubleshooting: Error storing passphrase in keyring (the sessions wrapping the secret does not exist) when unlocking a drive in Nautilus, and/or drives disappearing from the sidebar — Kill and restart Nautilus after installing gnome-keyring
Shell¶
CachyOS defaults to fish as the login shell, with cachyos-fish-config and
cachyos-zsh-config both installed.
This setup moves to zsh with oh-my-zsh and the powerlevel10k prompt. .zshrc and
.p10k.zsh, plus oh-my-zsh itself and all its plugins/theme, are managed via
chezmoi, with its source directory vendored inside this
repo at src/personal_os_setup/config/chezmoi/. oh-my-zsh/plugins/theme are declared as
git-repo externals in .chezmoiexternal.toml.
The app's "Sync dotfiles" tab lists every chezmoi-managed file as a checkbox (none
checked by default — pick which ones you mean to act on); use diff selected to
preview changes, apply selected to write them to your home directory, re-add
selected to pull live edits back into the repo, or forget selected to stop
tracking a file (the live file is left untouched — only the repo's copy is removed).
Check everything and run the corresponding action to reproduce the old whole-tree
chezmoi apply/diff/re-add behavior. To start tracking a new file, use
chezmoi: track a new file and enter its path — this copies it into the repo (never
edit a live dotfile directly and expect it to be under version control). chezmoi has
no auto-backup, so always run diff selected before apply selected.
set zsh as default shell remains a separate zsh action.
Theming¶
Noctalia can set application themes directly (Noctalia settings → Themes). Personal themes are kept rather than using the bundled ones.
Do not remove the Qt5/Qt6 theme packages — the Noctalia theme cannot currently be removed without affecting them.
Gaming¶
packages.yaml currently only installs steam under cachyos.pacman.Gaming — the pieces
below are not yet in the catalog and need to be added/installed later in the next update.
Base packages¶
cachyos-gaming-meta— CachyOS's own meta-package, pulls ingamemode,mangohud,lib32-mesa/lib32-vulkan-*and other 32-bit Proton/Wine runtime deps in one shot. Installing this covers most of the packages below without listing them individually.- CachyOS already ships a gaming-optimized kernel (
linux-cachyos, BORE/EEVDF scheduler) and repo-level-O3/x86-64-v3optimized packages by default — nothing to configure here, it's the baseline the whole distro is built on.
GameMode¶
gamemode temporarily applies CPU governor/scheduler/GPU tweaks while a game runs. Add
gamemoderun %command% as a Steam launch option per game, or gamemode --dlsym.
Conflict to know about: GameMode and ananicy-cpp both try to renice the same
processes. If ananicy-cpp is running (check systemctl status ananicy-cpp), stop/disable
it before relying on GameMode, or expect fights over process priority.
MangoHud¶
FPS/frametime/CPU/GPU overlay. mangohud %command% as a Steam launch option (combine with
GameMode: gamemode mangohud %command%), or mangohud --dlsym for OpenGL titles that need
the dlsym hook. Config: ~/.config/MangoHud/MangoHud.conf.
Proton¶
Prefer Proton-CachyOS (CachyOS's own Proton fork, tracks Proton's bleeding-edge branch) over stock Steam Proton for anything demanding — it merges fixes faster and, as of the mid-2026 update, auto-detects and enables HDR per-game with no launch options needed. Install/manage versions via ProtonUp-Qt (also handles GE-Proton if a specific game needs it instead). Set the version per-game in Steam: right-click → Properties → Compatibility.
HDR / 10-bit desktop¶
Quick visual check (two monitors, one HDR + one non-HDR): Play the same HDR YouTube video (e.g. https://youtu.be/MV5hhbqDNLs?t=145) at 2Min25 in a browser window on each display — one on the HDR-enabled monitor, one on a monitor still in SDR (or disable HDR in that monitor). Then drag the SDR video window over onto the HDR display, right next to the other one, and compare them side by side. If HDR is actually working, the window that started on the HDR display should look visibly brighter/punchier in highlights and more saturated than the dragged-over one, even though both are now physically on the same HDR-capable panel — because the dragged-in window was already tone-mapped down to SDR before the move, while the other was rendered with the real PQ curve and wide gamut from the start. For a rigorous, numeric confirmation (not just "does it look different"), use the mpv IPC method below.
Desktop HDR is a Hyprland output setting, independent of Steam/Proton — do this first if you want HDR games to actually look right, not just run:
- Enable HDR in the monitor's own OSD menu first if your monitor has this.
~/.config/hypr/config/monitors.lua, Real HDR needs bothbitdepth = 10andcm = "hdr"on the monitor block. nwg-displays' GUI does not exposecm, and might not preserve a manually-addedcmline the next time- Verify:
hyprctl monitors -j— the target output'scurrentFormatshould move offXRGB8888to a 10-bit format (e.g. XBGR2101010) once both fields are set. - If YouTube (or other browser video) starts buffering/stuttering after enabling HDR, reboot the machine and confirm.
- If the HDR monitor looks grey/washed out after the machine has been suspended (idle
long enough to trigger Noctalia's
lock-and-suspend, then resumed) — this is a known, confirmed Hyprland bug: HDR/color-management state doesn't get correctly re-committed to the display on resume, even though Hyprland's own config values are still correct (hyprctl monitors -jstill reports the rightsdrBrightness/sdrMaxLuminance/etc. — the panel just isn't showing it). See Washed out colors after resume from hibernation with monitor cm hdr · Issue #9724 and the related Discussion #10950 (same class of bug on any HDR state transition, not just resume). Confirmed fix on this machine: turn the monitor off and back on (forces it to re-read the DRM state). If that's inconvenient,hyprctl reloadis the documented lighter-weight workaround — try that first. - To confirm HDR is actually being decoded and displayed correctly end-to-end (not just that the monitor format changed), see Verifying HDR actually works in the apps doc — it queries mpv's live negotiated color state over IPC, rather than trusting the OSD.
With cm = "hdr" active, non-HDR apps (browser UI, etc.) aren't blasted at full HDR
brightness — Hyprland maps them into the sdrBrightness/sdrMaxLuminance slice above, while
surfaces that actually signal HDR metadata (e.g. a real HDR video) get their own negotiated range.
Verifying HDR actually works¶
End-to-end HDR (source → mpv → Hyprland → panel) was verified using this HDR10 test video: https://www.youtube.com/watch?v=njX2bu-_Vw4 — playing it in mpv fullscreen via yt-dlp, then querying mpv's live negotiated state over its JSON IPC socket rather than trusting the OSD:
- Launch mpv with an IPC socket so its internal state can be queried while playing:
- From another terminal, query the three relevant properties via
socat(orechopiped tonc -U): -
What each answers:
video-params— the source's real color metadata. HDR10 content should reportprimaries: bt.2020,gamma: pq, plus a masteringmax-luma/max-cll.video-out-params— what mpv actually sends to the display. If this matchesvideo-params(same primaries/gamma/max-luma), mpv is passing the real HDR signal through rather than tone-mapping it down to SDR before output.video-target-params— what mpv detected the display itself can do, negotiated live through Hyprland's Wayland color-management protocol (mpv needs--target-colorspace-hintfor this). A genuinebt.2020/pqresult with amax-lumaclose to the panel's real peak-nit spec (not clamped to ~225 or ~500 nits) confirms Hyprland is correctly advertising the display's HDR capability and mpv is targeting it.
-
A
solitaryBlockedBy: ['OPAQUE']flag may still show up in Hyprland's scene-graph debug output for the mpv surface — that only affects direct scanout eligibility (a compositing performance optimization), not the color/brightness pipeline, so it doesn't indicate broken HDR. - Quick on-screen sanity check without IPC: press
Iin mpv for the full stats overlay, which also lists the negotiated colorspace/gamma/luma fields (less detail than the IPC query, but fast to eyeball, and stays in view when using windowed test videos).
Testing this project in a VM¶
make vm-* targets (backed by scripts/vm.sh) spin up local KVM/QEMU/libvirt VMs to try
this project's setup in a disposable environment instead of on your real machine. Any Linux
host — make vm-deps auto-detects pacman/apt/dnf and checks the right package names for
each. On CachyOS and Ubuntu those packages are also in packages.yaml, so you can install them
from the app's own Packages tab instead of manually.
make vm-deps # verify qemu/libvirt/virt-manager/virt-viewer are installed, enable libvirtd
# (install any missing ones from the app's Packages tab first, if available for your distro)
make vm-cachyos # CachyOS VM -- manual ISO install, GUI console opens automatically
make vm-ubuntu-server # Ubuntu Server 26.04 -- unattended autoinstall, no clicking needed
make vm-ubuntu-server-manual # same ISO, manual click-through install
make vm-ubuntu # Ubuntu Desktop 26.04 -- manual install
make vm-list # list this project's VMs and their state
make vm-clean # destroy/undefine all this project's VMs, wipe disks (keeps cached ISOs)
make vm-clean-isos # delete cached install ISOs too (re-downloaded on next make vm-*)
Re-running a vm-* target on a VM that already exists just starts it and reopens the GUI
console instead of recreating it. Everything (ISOs, disk images, cloud-init seeds) is cached
under .vm/ (gitignored) — repo-local, so deleting the repo also cleans it all up.
If a VM has no internet access during install, it's very likely your host's firewall
blocking NAT'd VM traffic on libvirt's virbr0 bridge, even though libvirt sets up its own
NAT rules for it. On ufw-based hosts (Ubuntu's default), fix it with:
make vm-ufw-allow # asks for confirmation, then allows routing for virbr0 specifically
make vm-ufw-revert # undoes it, same confirm-first prompt
This only opens routing for virbr0 — every other interface keeps ufw's default DROP-forward
policy untouched. If your host uses firewalld instead (common on Fedora/RHEL), the equivalent
is adding virbr0's zone to the trusted zone or an explicit forwarding rule — vm-ufw-allow
doesn't cover that yet.
Apps shortcuts¶
For per-app configuration and shortcuts (Vicinae, Obsidian, PyCharm, Bitwarden, Nautilus, the terminal tools cheatsheet), plus a table of what's available on CachyOS vs. Windows/macOS and their alternatives, check apps_configuration_and_shorcuts.md.