Machine state management for Arch Linux with Sway (Wayland) desktop environment.
Ansible-driven provisioning and configuration for multiple workstations from a single repository.
sudo pacman -Syu --needed sudo vim git ansible just base-devel
git clone https://github.com/vix-4800/Workstation ~/Code/Workstation
cd ~/Code/Workstation
cp inventory/host_vars/localhost.yml.example inventory/host_vars/localhost.yml
cp vault/secrets.yml.example vault/secrets.yml
just vault-init
# edit inventory/host_vars/localhost.yml and vault/secrets.yml
just vault-encrypt
just deps
just applyjust apply # Full state: packages + configs + services
just sync # Only configs (fast, no package installs)
just plan # Dry-run: show what would change
just role shell # Apply specific role
just role desktop network # Multiple rolesjust vault-init # Create vault password file (first time)
just vault-encrypt # Encrypt vault/secrets.yml (after adding secrets in plain YAML)
just vault-edit # Edit encrypted secretsUse this order on a clean Arch install. just apply is not enough by itself on a fresh machine.
The target user must exist before running this repo and must be allowed to use sudo, because just apply runs
ansible-playbook --ask-become-pass.
Example:
useradd -m -G wheel -s /bin/bash <username>
passwd <username>
EDITOR=vim visudoEnable sudo for the wheel group in /etc/sudoers:
%wheel ALL=(ALL:ALL) ALL
Then log in as that user.
Install the base tools that are needed before this repository can manage the rest of the system:
sudo pacman -Syu --needed sudo vim git ansible just base-develThese are needed for:
sudo— privilege escalation for Ansible tasksvim— editinghost_vars, vault files, andvisudogit— cloning and updating the repositoryansible— running the playbookjust— command runner used by this repobase-devel— required for AUR builds duringyay/AUR tasks
git clone https://github.com/vix-4800/Workstation ~/Code/Workstation
cd ~/Code/WorkstationThis file is intentionally untracked and must be created locally on every machine:
cp inventory/host_vars/localhost.yml.example inventory/host_vars/localhost.yml
vim inventory/host_vars/localhost.ymlAt minimum, fill in the machine-specific values such as:
usernamehostnamecpu_vendorgpu_vendorhas_battery
site.yml always loads vault/secrets.yml, so the file must exist before you run the playbook.
cp vault/secrets.yml.example vault/secrets.yml
vim vault/secrets.yml
just vault-init
just vault-encryptAfter encryption, the file must start with $ANSIBLE_VAULT;.
This repository depends on Galaxy collections declared in requirements.yml,
including kewlfft.aur. On a clean system install them before the first apply:
just depsEquivalent command:
ansible-galaxy collection install -r requirements.ymljust applyEverything is managed through Ansible roles — packages, configs, services, and secrets. just provides a thin UX
layer on top of ansible-playbook.
The checked-in inventory targets localhost by default. Machine-specific hardware values live in
inventory/host_vars/localhost.yml, which is intentionally untracked; copy the example file on each machine and adjust
hostname, hardware flags, and any local overrides there.
inventory/
├── hosts.yml # Tracked inventory (defaults to localhost)
├── group_vars/all.yml # Shared: theme, font, shell, editor...
└── host_vars/
├── localhost.yml.example # Template for per-machine hardware values
└── localhost.yml # Local untracked copy
| Role | What it manages |
|---|---|
base |
Locale, hostname, timezone, essential packages (btop, dust, lm_sensors, yq, xdg-utils), env vars, fontconfig, vm.swappiness, paccache.timer |
yay |
AUR helper installation |
cpu |
CPU microcode (AMD/Intel); thermald thermal management service (Intel only) |
gpu |
GPU drivers + fan control (NVIDIA); DRM KMS modprobe and Wayland env config |
shell |
Fish, Bash, Kitty (default terminal), Alacritty, Tmux, aliases |
editor |
Neovim (with Lua + LuaRocks), VSCode flags, Git config |
audio |
PipeWire, WirePlumber, EasyEffects, multimedia apps |
network |
Firewall (nftables), Bluetooth (nm-applet + blueman tray flow), WireGuard (vault), V2RayA, USBGuard |
desktop |
SwayFX, Waybar, Wofi, SwayNC, GTKLock, Wlogout, polkit-gnome, XDG portals, swappy, wdisplays, qt5/6-wayland |
display-manager |
Greetd + ReGreet |
development |
PHP, Python, Go, Docker, linter configs |
appearance |
GTK themes (nwg-look), fonts, cursors, icons, wallpapers, plymouth |
apps |
Obsidian, Bitwarden, file-roller, gvfs-goa, Flatpak + Flathub, waypaper, TLP + Powertop (battery only) |
services |
All systemd user services and timers (xdg-user-dirs, etc.) |
ai-tools |
Claude Code, Codex, OpenCode, MCP/Serena, agent skills |
With has_battery: true, the apps role installs TLP and Powertop, disables existing
auto-cpufreq and power-profiles-daemon services, masks power-profiles-daemon when present
and the systemd rfkill service and socket,
and enables tlp.service. The retired auto-cpufreq package is no longer installed or enabled;
remove an existing installation with the migration commands below.
TLP uses its built-in power-saving defaults. Ansible deploys /etc/tlp.d/10-workstation.conf
with TLP_ENABLE=1, START_CHARGE_THRESH_BAT0=0 and STOP_CHARGE_THRESH_BAT0=90 to stop
charging the primary battery at 90% on supported hardware. Override apps_tlp_settings in local
host vars for hardware-specific settings. Settings in /etc/tlp.conf take precedence over this drop-in.
See the TLP configuration documentation.
Powertop is installed for interactive diagnostics with sudo powertop. It needs no repository
configuration or service. Do not enable powertop --auto-tune at boot: it overwrites settings
managed by TLP. See TLP and Powertop.
For an existing workstation, migrate before applying the updated roles:
sudo systemctl disable --now auto-cpufreq.service
sudo pacman -R auto-cpufreq
just role apps,services
systemctl --user daemon-reload
systemctl --user restart batsignal.service
sudo tlp-stat -s
sudo tlp-stat -cThe first two commands apply only if auto-cpufreq is still installed. The user service restart
loads the corrected batsignal command: notifications at 20% and 10%, and an unplug reminder at
90%. These notification thresholds do not control charging or trigger automatic suspend.
Tasks are tagged for granular execution. The common tags are role tags (desktop, network, development, etc.), type
tags (packages, config, services, system), and narrower subgroup tags such as wireguard, gtklock, or php.
| Tag | Scope |
|---|---|
config |
Deploy user-space config files (symlinks, no sudo) |
system |
System-level configs requiring sudo (/etc/, /boot/) |
packages |
Install packages |
services |
Manage systemd units |
| Role tags | shell, desktop, network, etc.; some tasks use only the role tag |
| Subgroup tags | wireguard, gtklock, php, docker, etc. |
WireGuard keys are stored in vault/secrets.yml, encrypted with ansible-vault. Per-host variables in
host_vars/localhost.yml and vault variables render the final configs.
vault/secrets.yml ──> host_vars/localhost.yml ──> templates/wg0.conf.j2 ──> /etc/wireguard/wg0.conf
(encrypted) (local vars) (Jinja2 template) (deployed config)
Create the vault file like this:
just vault-init
# create vault/secrets.yml from vault/secrets.yml.example and fill in your values
just vault-encrypt
just vault-editBefore running just vault-encrypt, vault/secrets.yml may exist temporarily as plain YAML copied from the example.
After that step, site.yml loads it through vars_files, so the file must remain actual ansible-vault ciphertext.
If you already created vault/secrets.yml as plain YAML, re-encrypt it:
ansible-vault encrypt vault/secrets.ymlYou can verify the file is encrypted if the first line starts with:
$ANSIBLE_VAULT;
├── Justfile # Command interface
├── ansible.cfg # Ansible settings
├── requirements.yml # Galaxy collections
├── site.yml # Master playbook
├── inventory/ # Machine definitions + variables
├── vault/ # Encrypted secrets
└── roles/
└── <role>/
├── tasks/main.yml # What to install and deploy
├── defaults/main.yml # Default variables
├── files/ # Config files (symlinked to ~/)
├── templates/ # Jinja2 templates
├── handlers/main.yml # Event handlers (daemon-reload, etc.)
└── scripts/ # Executable scripts (→ ~/.local/bin/)
# On machine A: edit the file (it's a symlink into the repo)
vim roles/desktop/files/waybar/style.css
git add -A && git commit -m "feat: update waybar style" && git push
# On machine B:
git pull && just syncAdd it to the appropriate role's tasks/main.yml, commit, push, and run just apply on both machines.
Host-specific state lives in the local untracked inventory/host_vars/localhost.yml. For example, base sets
hostname, GPU setup depends on gpu_vendor, battery services depend on has_battery, and WireGuard gets its secrets
from vault.
- SwayFX — Sway fork with blur, shadows, and rounded corners
- Waybar — status bar with custom scripts (weather, Docker, resources, network, audio)
- Wofi — launcher and selection menus used by Waybar scripts
- SwayNC — notification daemon
- Greetd + ReGreet — display manager
- GTKLock — screen locker with media and background modules
- Wlogout — session logout menu
- polkit-gnome — authentication agent (autostarted with Sway)
- xdg-desktop-portal-wlr — screensharing and file picker portals
- PipeWire + WirePlumber — audio server
- EasyEffects — per-application equaliser and audio effects
- NetworkManager — connection management
- network-manager-applet — tray-based GUI for joining and managing Wi-Fi networks
- Blueman — Bluetooth manager plus tray applet for paired devices
- WireGuard / V2RayA — VPN
- Swappy — screenshot annotation
- wdisplays — display configuration GUI
- nwg-look — GTK theme manager for Wayland
- Catppuccin Mocha — consistent colour theme across all components
- Kitty — default terminal for Sway, Wofi, and Waybar scripts; Fish, JetBrainsMono Nerd Font (14 pt),
Catppuccin Mocha, 80% background opacity, and 10,000 lines of scrollback.
The
shellrole linksroles/shell/files/kitty/to~/.config/kitty/. See the Kitty configuration reference for available settings. - Alacritty — remains installed with its existing configuration; available for manual launch.
- Obsidian — notes
- Bitwarden — password manager
- File Roller — archive manager
- Flatpak + Flathub — additional applications
- Arch Linux
- A local user account already created
- The user is in
wheel(or otherwise configured insudoers) sudo,vim,git,ansible,just, andbase-develinstalled- Internet connection
| Command | Description |
|---|---|
just apply |
Full state apply |
just sync |
Deploy configs only |
just plan |
Dry-run (check mode) |
just role <tags> |
Apply specific role(s) |
just deps |
Install Galaxy collections |
just vault-edit |
Edit encrypted secrets |
just vault-init |
Create vault password file |
just vault-encrypt |
Encrypt vault/secrets.yml |
just services |
Show running user services |
just molecule |
Run molecule tests |
just lint |
Lint playbooks and roles |
just check |
Syntax check only |