Files
omarchy-setup/install.sh
noconnorandClaude Opus 5.5 e1e5f05876 Capture omarchy-air's customizations as a per-host overlay
hosts/<hostname>/{config,system}/ now overlays the shared trees. install.sh
installs a host file in place of the shared copy at the same path, and
capture.sh pulls back into whichever copy the host installs from. That keeps
hardware-specific files off the wrong machine: the HP's EDID-named monitors.lua
and its fnmode=2 would otherwise land on the Air.

capture.sh refreshes the shared trees only on omarchy-laptop, where they were
captured, or when CAPTURE_SHARED=1 is set. On the Air, a plain run copied its
stock terminal, hypr and shell files over every one of the HP's customizations.

air's overlay holds monitors.lua (1.33333 scale and GDK_SCALE=1 for the
2560x1664 panel), input.lua (stock settings plus the inert-accel measurements),
herdr onboarding=false, git identity and credential helpers (no secrets),
mimeapps.list, and the Asahi installer's hid_apple fnmode=1 and show_notch=1.
bindings.lua is not overlaid because the shared one is a strict superset.
hosts/omarchy-air/README.md records what was left out on purpose and why, and
warns that the shared packages.txt is x86-specific.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKPyqMc7MGKyBLKVdeDejp
2026-10-16 15:49:19 -04:00

445 lines
17 KiB
Bash
Executable File

#!/bin/bash
# Apply this repo's Omarchy customizations to the machine it runs on.
#
# Safe to re-run: every step is a no-op when the target already matches, and any
# file it would overwrite is backed up to <file>.bak.<epoch> first.
#
# ./install.sh # everything
# ./install.sh config themes # only the named steps
# ./install.sh --list # show the steps
# ./install.sh --dry-run # print what would change, touch nothing
#
# Steps: packages config themes plugins bin services system
set -uo pipefail
REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
STAMP="$(date +%s)"
DRY=0
STEPS=(packages config themes plugins bin handoff services system)
# Per-host overlay: hosts/<hostname>/{config,system}/ mirror the shared trees, and
# a file there replaces the shared copy of the same path on that host only. It is
# how hardware-specific files (monitor scale, fnmode) stay off the wrong machine.
HOST="${OMARCHY_SETUP_HOST:-$(hostname)}"
HOST_DIR="$REPO/hosts/$HOST"
# omarchy-handoff peer addresses to open the firewall for. The peer is dual-homed
# (Wi-Fi + dock NIC) and the dock address disappears on undock, so list both.
if [[ -z ${HANDOFF_PEER_IPS+x} ]]; then
HANDOFF_PEER_IPS=(192.168.1.24 192.168.1.86)
fi
c_ok=$'\033[32m'; c_skip=$'\033[2m'; c_warn=$'\033[33m'; c_err=$'\033[31m'; c_hd=$'\033[1m'; c_off=$'\033[0m'
[[ -t 1 ]] || { c_ok=; c_skip=; c_warn=; c_err=; c_hd=; c_off=; }
say() { printf '%s\n' "$*"; }
head_() { printf '\n%s==> %s%s\n' "$c_hd" "$*" "$c_off"; }
ok() { printf ' %s+%s %s\n' "$c_ok" "$c_off" "$*"; }
skip() { printf ' %s= %s%s\n' "$c_skip" "$*" "$c_off"; }
warn() { printf ' %s! %s%s\n' "$c_warn" "$*" "$c_off"; }
err() { printf ' %sx %s%s\n' "$c_err" "$*" "$c_off"; }
run() { if (( DRY )); then printf ' %s? would: %s%s\n' "$c_skip" "$*" "$c_off"; else "$@"; fi; }
# install_file <src> <dest> [mode] -- copy only when content differs, backing up first
install_file() {
local src=$1 dest=$2 mode=${3:-}
local rel=${dest/#"$HOME"/\~}
if [[ -e $dest ]] && cmp -s "$src" "$dest"; then
skip "$rel"
return 0
fi
if (( DRY )); then
printf ' %s? would write %s%s\n' "$c_skip" "$rel" "$c_off"
return 0
fi
mkdir -p "$(dirname -- "$dest")"
if [[ -e $dest ]]; then
cp -a "$dest" "$dest.bak.$STAMP"
cp "$src" "$dest"
[[ -n $mode ]] && chmod "$mode" "$dest"
ok "$rel (previous saved as $(basename -- "$dest").bak.$STAMP)"
else
cp "$src" "$dest"
[[ -n $mode ]] && chmod "$mode" "$dest"
ok "$rel"
fi
}
# layered <tree> -- NUL-separated "<src>\t<rel>" for every file in the shared tree
# and the host overlay, the host copy winning where both have the same path.
layered() {
local tree=$1 f rel
declare -A pick=()
for base in "$REPO/$tree" "$HOST_DIR/$tree"; do
[[ -d $base ]] || continue
while IFS= read -r -d '' f; do
rel=${f#"$base/"}; pick[$rel]=$f
done < <(find "$base" -type f -print0)
done
for rel in "${!pick[@]}"; do printf '%s\t%s\0' "${pick[$rel]}" "$rel"; done | sort -z -t$'\t' -k2
}
# ---------------------------------------------------------------- packages
step_packages() {
head_ "Packages"
command -v omarchy >/dev/null || { err "omarchy not on PATH - is this an Omarchy system?"; return 1; }
local section="" repo=() aur=() optional=()
while IFS= read -r line; do
line=${line%%#*}; line=${line//[[:space:]]/}
[[ -z $line ]] && continue
case $line in
'[repo]') section=repo; continue ;;
'[aur]') section=aur; continue ;;
'[optional]') section=optional; continue ;;
esac
case $section in
repo) repo+=("$line") ;;
aur) aur+=("$line") ;;
optional) optional+=("$line") ;;
esac
done < "$REPO/packages.txt"
local missing=()
for p in "${repo[@]}"; do pacman -Qq "$p" &>/dev/null || missing+=("$p"); done
if (( ${#missing[@]} )); then
say " installing from repos: ${missing[*]}"
run omarchy pkg add "${missing[@]}" || err "some repo packages failed"
else
skip "all ${#repo[@]} repo packages present"
fi
missing=()
for p in "${aur[@]}"; do pacman -Qq "$p" &>/dev/null || missing+=("$p"); done
if (( ${#missing[@]} )); then
say " installing from AUR: ${missing[*]}"
run omarchy pkg aur add "${missing[@]}" || err "some AUR packages failed"
else
skip "all ${#aur[@]} AUR packages present"
fi
for p in "${optional[@]}"; do
pacman -Qq "$p" &>/dev/null || warn "optional, not installed: $p (omarchy pkg add $p)"
done
}
# ---------------------------------------------------------------- config
step_config() {
head_ "Config files -> ~/.config"
[[ -d $HOST_DIR/config ]] && say " host overlay: hosts/$HOST/config"
local src rel
while IFS=$'\t' read -r -d '' src rel; do
install_file "$src" "$HOME/.config/$rel"
done < <(layered config)
if (( ! DRY )) && command -v hyprctl >/dev/null && [[ -n ${HYPRLAND_INSTANCE_SIGNATURE:-} ]]; then
hyprctl reload >/dev/null 2>&1
local errs; errs=$(hyprctl configerrors 2>/dev/null)
if [[ -n $errs && $errs != "no errors"* ]]; then
err "Hyprland reported config errors:"; printf '%s\n' "$errs" | sed 's/^/ /'
else
ok "hyprctl reload - no config errors"
fi
fi
}
# ---------------------------------------------------------------- themes
step_themes() {
head_ "Themes -> ~/.config/omarchy/themes"
local t name dest
for t in "$REPO"/themes/*/; do
[[ -d $t ]] || continue
name=$(basename -- "$t")
dest="$HOME/.config/omarchy/themes/$name"
if [[ -d $dest ]] && diff -rq "$t" "$dest" >/dev/null 2>&1; then
skip "$name"
else
run mkdir -p "$dest"
run cp -r "$t." "$dest/"
ok "$name"
fi
done
say ""
say " Themes managed by aether (not in this repo - reinstall with \`aether\`):"
say " pink-gt3rs porsche-cayman porsche-rain"
say " Apply one with: omarchy theme set <name>"
}
# ---------------------------------------------------------------- plugins
step_plugins() {
head_ "Shell plugins -> ~/.config/omarchy/plugins"
local dir="$HOME/.config/omarchy/plugins"
[[ -d $dir ]] || run mkdir -p "$dir"
local id url sha p
while IFS=$'\t' read -r id url sha; do
[[ -z ${id:-} || $id == \#* ]] && continue
p="$dir/$id"
if [[ -d $p/.git ]]; then
if [[ $(git -C "$p" rev-parse HEAD 2>/dev/null) == "$sha" ]]; then
skip "$id @ ${sha:0:7}"
continue
fi
if [[ -n $(git -C "$p" status --porcelain 2>/dev/null) ]]; then
warn "$id has local changes - leaving it alone (wanted ${sha:0:7})"
continue
fi
run git -C "$p" fetch --quiet origin
run git -C "$p" checkout --quiet "$sha" && ok "$id -> ${sha:0:7}"
else
if (( DRY )); then
printf ' %s? would clone %s -> %s%s\n' "$c_skip" "$id" "${sha:0:7}" "$c_off"
continue
fi
git clone --quiet "$url" "$p" || { err "clone failed: $id ($url)"; continue; }
git -C "$p" checkout --quiet "$sha" || warn "$id: sha $sha not found, left on default branch"
ok "$id @ ${sha:0:7}"
fi
done < "$REPO/plugins.txt"
apply_herdr_hud
apply_worktree_patch io.github.tallsam.navbar-cat
say ""
say " Bar placement lives in config/omarchy/shell.json, already installed by the"
say " config step. The shell hot-reloads it; \`omarchy restart shell\` if not."
}
# indie.herdr-hud carries 10 commits that intentionally never went to the
# upstream PR, plus an uncommitted working tree. Both are replayed here.
apply_herdr_hud() {
local p="$HOME/.config/omarchy/plugins/indie.herdr-hud"
local bundle="$REPO/plugin-patches/indie.herdr-hud.bundle"
[[ -d $p/.git && -f $bundle ]] || return 0
if git -C "$p" rev-parse --verify --quiet local >/dev/null; then
skip "indie.herdr-hud: 'local' branch already present"
elif (( DRY )); then
printf ' %s? would replay the herdr-hud local branch%s\n' "$c_skip" "$c_off"
return 0
else
git -C "$p" fetch --quiet "$bundle" 'refs/heads/local:refs/heads/local' \
&& { git -C "$p" checkout --quiet local && ok "indie.herdr-hud: replayed 'local' branch"; } \
|| warn "indie.herdr-hud: could not replay the local branch from the bundle"
fi
apply_worktree_patch indie.herdr-hud
install_file "$REPO/plugin-patches/indie.herdr-hud--bin--herdr-agents" "$p/bin/herdr-agents" 755
}
apply_worktree_patch() {
local id=$1
local p="$HOME/.config/omarchy/plugins/$id"
local patch="$REPO/plugin-patches/$id-worktree.patch"
[[ -d $p/.git && -s $patch ]] || return 0
if git -C "$p" apply --reverse --check "$patch" &>/dev/null; then
skip "$id: working-tree patch already applied"
elif (( DRY )); then
printf ' %s? would apply %s working-tree patch%s\n' "$c_skip" "$id" "$c_off"
elif git -C "$p" apply --3way "$patch" &>/dev/null; then
ok "$id: applied working-tree patch"
else
warn "$id: working-tree patch did not apply cleanly - see $patch"
fi
}
# ---------------------------------------------------------------- bin
step_bin() {
head_ "Scripts -> ~/.local/bin"
local f
for f in "$REPO"/bin/*; do
[[ -f $f ]] || continue
install_file "$f" "$HOME/.local/bin/$(basename -- "$f")" 755
done
case ":$PATH:" in
*":$HOME/.local/bin:"*) ;;
*) warn "~/.local/bin is not on PATH" ;;
esac
say ""
say " fix-stuck-displays needs passwordless sudo for 'tee /sys/class/drm/*/status'."
say " omarchy-handoff needs a peer and a paired fingerprint before it does anything:"
say " omarchy-handoff peer <hostname> <ip> then omarchy-handoff doctor"
say " Its config (~/.config/lan-mouse/) holds a private key and per-machine"
say " fingerprints, so it is not in this repo by design."
}
# ---------------------------------------------------------------- services
step_services() {
head_ "systemd user units"
local f name changed=0
for f in "$REPO"/systemd/user/*.service; do
[[ -f $f ]] || continue
name=$(basename -- "$f")
local dest="$HOME/.config/systemd/user/$name"
[[ -e $dest ]] && cmp -s "$f" "$dest" || changed=1
install_file "$f" "$dest"
done
(( changed )) && run systemctl --user daemon-reload
for f in "$REPO"/systemd/user/*.service; do
[[ -f $f ]] || continue
name=$(basename -- "$f")
if systemctl --user is-enabled --quiet "$name" 2>/dev/null; then
skip "$name already enabled"
else
run systemctl --user enable "$name" && ok "enabled $name"
fi
done
say ""
say " Both are WantedBy=graphical-session.target, so they start with Hyprland"
say " rather than now. 'systemctl --user start <unit>' to bring one up early."
}
# ---------------------------------------------------------------- handoff
step_handoff() {
head_ "omarchy-handoff (lan-mouse)"
# Self-sufficient on purpose: `install.sh handoff` alone brings input sharing up
# on an otherwise stock machine, without the packages/config/themes steps that
# would drag personalizations along. Everything here is idempotent, so running
# the full install.sh afterwards does no extra work.
# lan-mouse: the signed package, for the GUI/CLI and as the unpatched fallback.
# rust: only to compile handoff/patch/, removable afterwards.
# NOT python-evdev: packages.txt calls it a dependency, but omarchy-air runs
# this whole setup without it installed, so it is not required for sharing.
local need=() p
for p in lan-mouse rust; do
pacman -Qq "$p" &>/dev/null || need+=("$p")
done
if (( ${#need[@]} )); then
say " installing: ${need[*]}"
run omarchy pkg add "${need[@]}" || warn "package install failed: ${need[*]}"
else
skip "lan-mouse, rust"
fi
install_file "$REPO/bin/omarchy-handoff" "$HOME/.local/bin/omarchy-handoff" 0755
# 1. The patched daemon. systemd/user/lan-mouse.service points at this path, so
# it has to exist before the services step enables the unit. Per-architecture:
# this laptop is x86_64 and omarchy-air is aarch64, so each builds its own.
if [[ -x $HOME/.local/bin/lan-mouse-patched ]]; then
skip "~/.local/bin/lan-mouse-patched (delete it to force a rebuild)"
elif ! command -v cargo >/dev/null; then
warn "cargo missing - run the packages step first, or: omarchy pkg add rust"
elif (( DRY )); then
printf ' %s? would build ~/.local/bin/lan-mouse-patched from handoff/patch/%s\n' "$c_skip" "$c_off"
else
say " building patched daemon from handoff/patch/ (~1 min; see handoff/patch/REBUILD.md for why)"
if "$REPO/handoff/patch/build.sh"; then ok "~/.local/bin/lan-mouse-patched"
else err "build failed - the unit will not start until this succeeds"; fi
fi
# 2. The unit, which points at the binary built above.
install_file "$REPO/systemd/user/lan-mouse.service" \
"$HOME/.config/systemd/user/lan-mouse.service" 0644
if (( ! DRY )); then
systemctl --user daemon-reload
systemctl --user is-enabled --quiet lan-mouse.service 2>/dev/null \
&& skip "lan-mouse.service already enabled" \
|| { systemctl --user enable lan-mouse.service >/dev/null && ok "enabled lan-mouse.service"; }
fi
# 3. The firewall. THE bug that cost a full day: ufw is default-deny inbound, so
# the peer's DTLS handshake dies at the wire and presents EXACTLY like a bad
# certificate. Authorization is receiver-side only (connect.rs sets
# insecure_skip_verify), so the RECEIVER's firewall must allow 4242/udp or
# that direction is silently dead.
if ! command -v ufw >/dev/null; then
skip "ufw not installed - nothing to open"
else
local ip rules=""
# Both reading and changing the ruleset need root. If sudo can't run without
# a prompt and there's no terminal to prompt on, say exactly what to run
# instead of emitting raw sudo errors and half-applying.
if (( ! DRY )) && ! sudo -n true 2>/dev/null && [[ ! -t 0 ]]; then
warn "sudo needs a password and there is no terminal; run these yourself:"
for ip in "${HANDOFF_PEER_IPS[@]}"; do
say " sudo ufw allow from $ip to any port 4242 proto udp comment 'lan-mouse peer $ip'"
done
say ""
say " Without them, anything the peer INITIATES toward this machine is"
say " dropped at the wire and looks exactly like a bad certificate."
return 0
fi
(( DRY )) || rules=$(sudo ufw status 2>/dev/null || true)
for ip in "${HANDOFF_PEER_IPS[@]}"; do
if [[ $rules == *"4242"*"$ip"* ]]; then
skip "ufw allows 4242/udp from $ip"
elif (( DRY )); then
printf ' %s? would allow 4242/udp from %s%s\n' "$c_skip" "$ip" "$c_off"
else
sudo ufw allow from "$ip" to any port 4242 proto udp \
comment "lan-mouse peer $ip" >/dev/null && ok "ufw: 4242/udp from $ip"
fi
done
say ""
say " Verify a blocked handshake with: journalctl -k -g 'UFW BLOCK'"
fi
say ""
say " ~/.config/lan-mouse/ is NOT in this repo (private key + per-machine"
say " fingerprints). Start from handoff/config/config.toml.example-* and:"
say " omarchy-handoff fingerprint # give it to the peer, LOWERCASE"
say " omarchy-handoff pair <peer> <lowercase-sha256>"
say " omarchy-handoff doctor # must pass before arming"
say " omarchy-handoff on"
say " A fresh install regenerates the cert, so the PEER must re-pair."
}
# ---------------------------------------------------------------- system
step_system() {
head_ "System drop-ins (needs sudo)"
[[ -d $HOST_DIR/system ]] && say " host overlay: hosts/$HOST/system"
local src rel dest
while IFS=$'\t' read -r -d '' src rel; do
dest="/etc/$rel"
if [[ -e $dest ]] && cmp -s "$src" "$dest"; then
skip "$dest"
continue
fi
if (( DRY )); then
printf ' %s? would write %s%s\n' "$c_skip" "$dest" "$c_off"
continue
fi
sudo install -D -m 0644 "$src" "$dest" && ok "$dest"
done < <(layered system)
if (( ! DRY )); then
sudo sysctl --system >/dev/null 2>&1 && ok "sysctl reloaded"
say ""
say " hid_apple.conf (fnmode) only takes effect once the"
say " initramfs is rebuilt or the module reloads:"
say " sudo mkinitcpio -P # or: sudo modprobe -r hid_apple && sudo modprobe hid_apple"
fi
}
# ---------------------------------------------------------------- main
main() {
local want=()
for a in "$@"; do
case $a in
--dry-run|-n) DRY=1 ;;
--list|-l) printf '%s\n' "${STEPS[@]}"; exit 0 ;;
-h|--help) sed -n '2,15p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
-*) err "unknown option: $a"; exit 2 ;;
*)
if [[ " ${STEPS[*]} " == *" $a "* ]]; then want+=("$a")
else err "unknown step: $a (one of: ${STEPS[*]})"; exit 2; fi ;;
esac
done
(( ${#want[@]} )) || want=("${STEPS[@]}")
say "${c_hd}omarchy-setup${c_off} repo=$REPO host=$HOST"
(( DRY )) && warn "dry run - nothing will be changed"
command -v omarchy >/dev/null || warn "omarchy not found; this looks like a non-Omarchy host"
for s in "${want[@]}"; do "step_$s"; done
head_ "Done"
say " Re-running is safe. Backups from this run are suffixed .bak.$STAMP"
say " Pull live changes back into the repo with ./capture.sh"
}
main "$@"