A fresh generic install wants input sharing without the packages, config, themes or plugins steps dragging personalizations along, so the handoff step now stands alone: it installs lan-mouse and rust, drops in the wrapper, builds the patch, installs and enables the unit, and opens the firewall. Everything is idempotent, so a later full run does no extra work. Verified by running it twice on omarchy-air -- second pass is all skips, and the daemon comes back armed with a real barrier. Drops python-evdev from the required set. packages.txt calls it a lan-mouse dependency, but omarchy-air runs this entire setup without it installed, so it is not needed for sharing and a minimal install should not pull it. The ufw section now degrades instead of half-applying: if sudo needs a password and there is no terminal to prompt on, it prints the exact commands to run and says what breaks without them, rather than emitting raw sudo errors. Hardware note for whoever reads this next: Omarchy's own installer covers most of what packages.txt lists under hardware. install/hardware has per-vendor trees (apple, asus, framework, intel, lenovo) plus vulkan.sh, zram.sh, fix-fkeys.sh, sof-firmware.sh and thermald.sh, and the installer prompts for keyboard, user, hostname and timezone. So vulkan-intel, sof-firmware, thermald, zram-generator and the hid_apple fnmode drop-in are all redundant on a fresh install, and the intel-* entries are wrong on non-Intel hardware. Apply the hardware block only on the machine it was captured from. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrgrMhPG8CTo9XZF326rjZ
425 lines
16 KiB
Bash
Executable File
425 lines
16 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)
|
|
|
|
# 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
|
|
}
|
|
|
|
# ---------------------------------------------------------------- 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"
|
|
local src rel
|
|
while IFS= read -r -d '' src; do
|
|
rel=${src#"$REPO/config/"}
|
|
install_file "$src" "$HOME/.config/$rel"
|
|
done < <(find "$REPO/config" -type f -print0 | sort -z)
|
|
|
|
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)"
|
|
local src rel dest
|
|
while IFS= read -r -d '' src; do
|
|
rel=${src#"$REPO/system/"}
|
|
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 < <(find "$REPO/system" -type f -print0 | sort -z)
|
|
|
|
if (( ! DRY )); then
|
|
sudo sysctl --system >/dev/null 2>&1 && ok "sysctl reloaded"
|
|
say ""
|
|
say " hid_apple.conf (fnmode=2: F-keys act as F-keys) 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"
|
|
(( 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 "$@"
|