Skip to content

Keep an Omarchy workstation reproducible

Omarchy sets up Arch Linux as a complete desktop. What you add on top (packages, dotfiles, a timer or two, something from the AUR) is usually done by hand and remembered by nobody. This guide keeps that layer in a recipe: a fresh Omarchy install plus sadame apply gives the same machine, and sadame drift answers "did I break my laptop?" in one command. It uses examples/12-omarchy-workstation.

The recipe

examples/12-omarchy-workstation/recipe.rb
# A personal Arch or Omarchy workstation, applied locally:
#
#   sudo sadame apply examples/12-omarchy-workstation
#
# Packages from the official repositories, dotfiles from files/ into your
# home directory, a systemd user timer, and an AUR package through yay.
# Run `sudo sadame drift ...` now and then: exit 0 means the laptop still
# matches the recipe.
raise "this example is for Arch Linux and Omarchy, not #{platform.description}" unless platform.family == "arch"

me = vars[:user]
raise "run it with sudo, or pass --var user=NAME" if me.nil? || me.empty? || me == "root"

home = "/home/#{me}"

# Packages: only what you want on top of Arch or Omarchy.
vars[:packages].each { |name| package name }

# Dotfiles. Directories are listed so that they belong to you, not root.
["#{home}/.config", "#{home}/.config/systemd", "#{home}/.config/systemd/user", "#{home}/.local",
 "#{home}/.local/bin"].each do |path|
  directory path, owner: me, mode: "0755"
end
file "#{home}/.gitconfig", template: "gitconfig.erb", owner: me, mode: "0644"
file "#{home}/.config/starship.toml", source: "starship.toml", owner: me, mode: "0644"
file "#{home}/.local/bin/update-system", source: "update-system", owner: me, mode: "0755"

# A user timer that empties ~/Downloads of files older than 30 days. Your
# systemd instance must run even when you are logged out, so lingering is
# turned on and the instance started.
exec "enable lingering for #{me}",
     command: "loginctl enable-linger #{me}",
     creates: "/var/lib/systemd/linger/#{me}"
exec "start #{me}'s systemd instance",
     command: "systemctl start user@$(id -u #{me}).service",
     not_if: "systemctl is-active --quiet user@$(id -u #{me}).service"
exec "reload #{me}'s systemd units",
     command: "systemctl --user -M #{me}@ daemon-reload",
     action: :nothing
file "#{home}/.config/systemd/user/clean-downloads.service",
     source: "clean-downloads.service",
     owner: me,
     mode: "0644",
     notifies: [:run, "exec[reload #{me}'s systemd units]", :immediately]
file "#{home}/.config/systemd/user/clean-downloads.timer",
     source: "clean-downloads.timer",
     owner: me,
     mode: "0644",
     notifies: [:run, "exec[reload #{me}'s systemd units]", :immediately]
service "clean-downloads.timer", user: me, enable: true, state: :running

# An AUR package. yay builds as you and asks sudo for pacman, so this needs
# passwordless sudo for pacman or a recent sudo timestamp. Skipped where
# yay is not installed, such as a plain Arch container.
vars[:aur_packages].each do |name|
  exec "install #{name} from the AUR",
       command: "runuser -u #{me} -- yay -S --needed --noconfirm #{name}",
       only_if: "command -v yay",
       not_if: "pacman -Q #{name}"
end
examples/12-omarchy-workstation/vars.rb
# Run with sudo: SUDO_USER is the user whose home directory is managed.
{
  user: ENV["SUDO_USER"],
  git_name: "Your Name",
  git_email: "you@example.com",
  packages: ["git", "htop", "ripgrep", "fd", "bat", "starship"],
  aur_packages: ["visual-studio-code-bin"]
}

Copy the directory somewhere you keep under version control, edit vars.rb and the files in files/ and templates/, and add your own.

What belongs in the recipe, and what does not

In the recipe Left to Omarchy
packages you install on top of Omarchy the packages Omarchy installs
your dotfiles: git identity, shell prompt, editor settings Omarchy's own configuration (Hyprland, Waybar, its themes)
your scripts in ~/.local/bin Omarchy's scripts and its update mechanism
your systemd user units and timers Omarchy's services
AUR packages you rely on yay itself, which Omarchy ships

The rule of thumb: manage what you would have to redo by hand after a reinstall, and leave the files Omarchy installs and updates alone. If the recipe managed them too, every Omarchy update would show up as drift, and every apply would undo the update. When you do change one of Omarchy's settings, make the change the way Omarchy's documentation describes for local changes, so its updates and your recipe do not overwrite each other, and manage only your part.

Running it

On a fresh install, the plan shows everything the recipe will add. Run it with sudo: packages need root, and SUDO_USER tells the recipe whose home directory to manage. The output below comes from an Arch Linux ARM container on an ARM64 machine, where yay is missing, so the AUR step is skipped; on Omarchy it runs.

$ sudo sadame plan examples/12-omarchy-workstation
sadame 0.5.1 plan on local (Arch Linux ARM)

+ package[git]
    state: absent -> installed

+ package[htop]
    state: absent -> installed

+ package[ripgrep]
    state: absent -> installed

+ package[fd]
    state: absent -> installed

+ package[bat]
    state: absent -> installed

+ package[starship]
    state: absent -> installed

+ directory[/home/alice/.config]
    state: absent -> present
    mode: 0755
    owner: alice

+ directory[/home/alice/.config/systemd]
    state: absent -> present
    mode: 0755
    owner: alice

+ directory[/home/alice/.config/systemd/user]
    state: absent -> present
    mode: 0755
    owner: alice

+ directory[/home/alice/.local]
    state: absent -> present
    mode: 0755
    owner: alice

+ directory[/home/alice/.local/bin]
    state: absent -> present
    mode: 0755
    owner: alice

+ file[/home/alice/.gitconfig]
    state: absent -> present
    mode: 0644
    owner: alice
    --- /dev/null
    +++ /home/alice/.gitconfig (desired)
    @@ -0,0 +1,8 @@
    +# Managed by sadame. Local changes will be overwritten.
    +[user]
    +   name = Your Name
    +   email = you@example.com
    +[init]
    +   defaultBranch = main
    +[pull]
    +   rebase = true

+ file[/home/alice/.config/starship.toml]
    state: absent -> present
    mode: 0644
    owner: alice
    --- /dev/null
    +++ /home/alice/.config/starship.toml (desired)
    @@ -0,0 +1,5 @@
    +# Managed by sadame.
    +add_newline = false
    +
    +[directory]
    +truncation_length = 3

+ file[/home/alice/.local/bin/update-system]
    state: absent -> present
    mode: 0755
    owner: alice
    --- /dev/null
    +++ /home/alice/.local/bin/update-system (desired)
    @@ -0,0 +1,7 @@
    +#!/bin/sh
    +# Updates the system packages, then the AUR packages if yay is installed.
    +set -e
    +sudo pacman -Syu
    +if command -v yay >/dev/null 2>&1; then
    +  yay -Sua
    +fi

! exec[enable lingering for alice]
    command: loginctl enable-linger alice
    (/var/lib/systemd/linger/alice does not exist)

! exec[start alice's systemd instance]
    command: systemctl start user@$(id -u alice).service
    (not_if exited non-zero)

+ file[/home/alice/.config/systemd/user/clean-downloads.service]
    state: absent -> present
    mode: 0644
    owner: alice
    --- /dev/null
    +++ /home/alice/.config/systemd/user/clean-downloads.service (desired)
    @@ -0,0 +1,6 @@
    +[Unit]
    +Description=Remove files older than 30 days from ~/Downloads
    +
    +[Service]
    +Type=oneshot
    +ExecStart=/usr/bin/find %h/Downloads -type f -mtime +30 -delete
    notifies: run exec[reload alice's systemd units] (immediately)

+ file[/home/alice/.config/systemd/user/clean-downloads.timer]
    state: absent -> present
    mode: 0644
    owner: alice
    --- /dev/null
    +++ /home/alice/.config/systemd/user/clean-downloads.timer (desired)
    @@ -0,0 +1,9 @@
    +[Unit]
    +Description=Clean ~/Downloads weekly
    +
    +[Timer]
    +OnCalendar=weekly
    +Persistent=true
    +
    +[Install]
    +WantedBy=timers.target
    notifies: run exec[reload alice's systemd units] (immediately)

~ service[clean-downloads.timer]
    enabled: false -> true
    state: stopped -> running

Plan: 16 to create, 1 to update, 0 to remove, 2 to run, 2 unchanged.
$ sudo sadame apply examples/12-omarchy-workstation
sadame 0.5.1 apply on local (Arch Linux ARM)

changed    package[git]
           state: absent -> installed
changed    package[htop]
           state: absent -> installed
changed    package[ripgrep]
           state: absent -> installed
changed    package[fd]
           state: absent -> installed
changed    package[bat]
           state: absent -> installed
changed    package[starship]
           state: absent -> installed
changed    directory[/home/alice/.config]
           state: absent -> present
           mode: 0755
           owner: alice
changed    directory[/home/alice/.config/systemd]
           state: absent -> present
           mode: 0755
           owner: alice
changed    directory[/home/alice/.config/systemd/user]
           state: absent -> present
           mode: 0755
           owner: alice
changed    directory[/home/alice/.local]
           state: absent -> present
           mode: 0755
           owner: alice
changed    directory[/home/alice/.local/bin]
           state: absent -> present
           mode: 0755
           owner: alice
changed    file[/home/alice/.gitconfig]
           state: absent -> present
           mode: 0644
           owner: alice
           --- /dev/null
           +++ /home/alice/.gitconfig (desired)
           @@ -0,0 +1,8 @@
           +# Managed by sadame. Local changes will be overwritten.
           +[user]
           +    name = Your Name
           +    email = you@example.com
           +[init]
           +    defaultBranch = main
           +[pull]
           +    rebase = true
changed    file[/home/alice/.config/starship.toml]
           state: absent -> present
           mode: 0644
           owner: alice
           --- /dev/null
           +++ /home/alice/.config/starship.toml (desired)
           @@ -0,0 +1,5 @@
           +# Managed by sadame.
           +add_newline = false
           +
           +[directory]
           +truncation_length = 3
changed    file[/home/alice/.local/bin/update-system]
           state: absent -> present
           mode: 0755
           owner: alice
           --- /dev/null
           +++ /home/alice/.local/bin/update-system (desired)
           @@ -0,0 +1,7 @@
           +#!/bin/sh
           +# Updates the system packages, then the AUR packages if yay is installed.
           +set -e
           +sudo pacman -Syu
           +if command -v yay >/dev/null 2>&1; then
           +  yay -Sua
           +fi
changed    exec[enable lingering for alice]
           command: loginctl enable-linger alice
           (/var/lib/systemd/linger/alice does not exist)
changed    exec[start alice's systemd instance]
           command: systemctl start user@$(id -u alice).service
           (not_if exited non-zero)
unchanged  exec[reload alice's systemd units]
changed    file[/home/alice/.config/systemd/user/clean-downloads.service]
           state: absent -> present
           mode: 0644
           owner: alice
           --- /dev/null
           +++ /home/alice/.config/systemd/user/clean-downloads.service (desired)
           @@ -0,0 +1,6 @@
           +[Unit]
           +Description=Remove files older than 30 days from ~/Downloads
           +
           +[Service]
           +Type=oneshot
           +ExecStart=/usr/bin/find %h/Downloads -type f -mtime +30 -delete
notified   exec[reload alice's systemd units]: run (immediately, from file[/home/alice/.config/systemd/user/clean-downloads.service])
changed    file[/home/alice/.config/systemd/user/clean-downloads.timer]
           state: absent -> present
           mode: 0644
           owner: alice
           --- /dev/null
           +++ /home/alice/.config/systemd/user/clean-downloads.timer (desired)
           @@ -0,0 +1,9 @@
           +[Unit]
           +Description=Clean ~/Downloads weekly
           +
           +[Timer]
           +OnCalendar=weekly
           +Persistent=true
           +
           +[Install]
           +WantedBy=timers.target
notified   exec[reload alice's systemd units]: run (immediately, from file[/home/alice/.config/systemd/user/clean-downloads.timer])
changed    service[clean-downloads.timer]
           enabled: false -> true
           state: stopped -> running
unchanged  exec[install visual-studio-code-bin from the AUR]

Summary
  changed     19  package[git]
                  package[htop]
                  package[ripgrep]
                  package[fd]
                  package[bat]
                  package[starship]
                  directory[/home/alice/.config]
                  directory[/home/alice/.config/systemd]
                  directory[/home/alice/.config/systemd/user]
                  directory[/home/alice/.local]
                  directory[/home/alice/.local/bin]
                  file[/home/alice/.gitconfig]
                  file[/home/alice/.config/starship.toml]
                  file[/home/alice/.local/bin/update-system]
                  exec[enable lingering for alice]
                  exec[start alice's systemd instance]
                  file[/home/alice/.config/systemd/user/clean-downloads.service]
                  file[/home/alice/.config/systemd/user/clean-downloads.timer]
                  service[clean-downloads.timer]
  unchanged    2
  failed       0
  skipped      0
  notified     2  exec[reload alice's systemd units]: run
                  exec[reload alice's systemd units]: run

Finished in 37.34s.

The two notified lines are systemctl --user daemon-reload running right after each unit file was written (:immediately), before the timer is enabled.

Did I break my laptop?

$ sudo sadame drift examples/12-omarchy-workstation
sadame 0.5.1 drift on local (Arch Linux ARM)

No drift: 21 resources match the recipe.

$ echo $?
0

Now edit ~/.gitconfig by hand and delete a script, as one does:

$ sed -i 's/rebase = true/rebase = false/' ~/.gitconfig

$ rm ~/.local/bin/update-system

$ sudo sadame drift examples/12-omarchy-workstation
sadame 0.5.1 drift on local (Arch Linux ARM)

~ file[/home/alice/.gitconfig]
    --- /home/alice/.gitconfig
    +++ /home/alice/.gitconfig (desired)
    @@ -5,4 +5,4 @@
     [init]
        defaultBranch = main
     [pull]
    -   rebase = false
    +   rebase = true

+ file[/home/alice/.local/bin/update-system]
    state: absent -> present
    mode: 0755
    owner: alice
    --- /dev/null
    +++ /home/alice/.local/bin/update-system (desired)
    @@ -0,0 +1,7 @@
    +#!/bin/sh
    +# Updates the system packages, then the AUR packages if yay is installed.
    +set -e
    +sudo pacman -Syu
    +if command -v yay >/dev/null 2>&1; then
    +  yay -Sua
    +fi

Drift: 2 of 21 resources differ from the recipe.

$ echo $?
1

apply puts both back:

$ sudo sadame apply examples/12-omarchy-workstation
sadame 0.5.1 apply on local (Arch Linux ARM)

unchanged  package[git]
unchanged  package[htop]
unchanged  package[ripgrep]
unchanged  package[fd]
unchanged  package[bat]
unchanged  package[starship]
unchanged  directory[/home/alice/.config]
unchanged  directory[/home/alice/.config/systemd]
unchanged  directory[/home/alice/.config/systemd/user]
unchanged  directory[/home/alice/.local]
unchanged  directory[/home/alice/.local/bin]
changed    file[/home/alice/.gitconfig]
           --- /home/alice/.gitconfig
           +++ /home/alice/.gitconfig (desired)
           @@ -5,4 +5,4 @@
            [init]
                defaultBranch = main
            [pull]
           -    rebase = false
           +    rebase = true
unchanged  file[/home/alice/.config/starship.toml]
changed    file[/home/alice/.local/bin/update-system]
           state: absent -> present
           mode: 0755
           owner: alice
           --- /dev/null
           +++ /home/alice/.local/bin/update-system (desired)
           @@ -0,0 +1,7 @@
           +#!/bin/sh
           +# Updates the system packages, then the AUR packages if yay is installed.
           +set -e
           +sudo pacman -Syu
           +if command -v yay >/dev/null 2>&1; then
           +  yay -Sua
           +fi
unchanged  exec[enable lingering for alice]
unchanged  exec[start alice's systemd instance]
unchanged  exec[reload alice's systemd units]
unchanged  file[/home/alice/.config/systemd/user/clean-downloads.service]
unchanged  file[/home/alice/.config/systemd/user/clean-downloads.timer]
unchanged  service[clean-downloads.timer]
unchanged  exec[install visual-studio-code-bin from the AUR]

Summary
  changed      2  file[/home/alice/.gitconfig]
                  file[/home/alice/.local/bin/update-system]
  unchanged   19
  failed       0
  skipped      0
  notified     0

Finished in 0.11s.

Running sudo sadame drift ... from time to time, or from a systemd user timer, tells you when the machine has wandered from what you wrote down.

From a fresh install to the same machine

  1. Install Omarchy.
  2. Clone the repository with your recipe.
  3. Install sadame (see Install).
  4. sudo sadame apply path/to/your-recipe
  5. sudo sadame drift path/to/your-recipe; exit status 0 means done.

Common mistakes

  • Running without sudo, or as root directly. Without sudo, installing packages fails; as root without sudo, SUDO_USER is empty and the recipe stops. Pass --var user=NAME if you must.
  • The AUR step asking for a password. yay runs sudo pacman, and sadame cannot answer a prompt. Run sudo -v just before, or allow pacman without a password.
  • Managing Omarchy's own files. See the table above.
  • Upgrading packages with the recipe. package means "installed", not "latest"; keep updating with pacman -Syu (or update-system from the example).

See also