Files
omarchy-setup/install.sh
T
noconnorandClaude Opus 5 50176b78ee Sync omarchy-air's handoff work: pointer patch, firewall, case fix
Everything from the air side of lan-mouse that was living only on that
machine, plus corrections to what this repo already said.

New: handoff/patch/ is a two-line source patch making the daemon forward
the ACCELERATED pointer deltas. Upstream 0.11.0 forwards dx_unaccel and
discards the accelerated pair in the same event, and the wlr receiver
applies no curve either, so remote motion arrives with the sender's
acceleration stripped and nothing replacing it -- and sensitivity /
accel_profile are inert on both ends, so no config can fix it. The patch
also adds LAN_MOUSE_POINTER_SCALE for the residual logical-ppi gap
(0.7 for air's 140.1ppi against the LG's ~95.8). install.sh builds it
per-machine; the unit now runs the result. This laptop is still unpatched,
so its forwarded motion is still wrong until the handoff step runs here.

New: install.sh --handoff opens 4242/udp for the peer in ufw. ufw is
default-deny inbound and a dropped inbound packet presents IDENTICALLY to
a bad certificate; this cost a day. Nothing in the repo covered it.

Fixed: cmd_restart bypassed the systemd unit with pkill + uwsm-app, which
races Restart=on-failure and can leave two daemons on 4242. It now prefers
the unit, matching what cmd_log already assumed, and keeps the hand-start
path only for when no unit exists.

Documented, and this was the actual root cause of a day of dead crossings:
fingerprints are CASE-SENSITIVE. lan-mouse generates them lowercase and
compares with a plain map lookup, while openssl prints uppercase and
cli authorize-key stores what it is handed. An uppercase digest is the
right digest, valid TOML, non-empty, passes every guard, and never
matches -- the handshake dies bad_certificate. Also documented:
authorization is receiver-side only (connect.rs sets insecure_skip_verify),
so each direction depends solely on the receiver's list and a sender cannot
tell whether it is talking to the real peer.

Corrected: README claimed the unknown-device* accel rule was needed because
the receiver double-accelerates already-accelerated deltas. That is backwards
and the rule is a measured no-op -- the device matches, but sensitivity and
accel_profile are libinput settings and a wlr-virtual-pointer is not a
libinput device. Measured on both machines: 398x198px travel at
sensitivity 0 and identical at -0.9, exactly 1:1. Lines kept (harmless, and
the naming note is worth having) with the real mechanism written down.

~/.config/lan-mouse/ stays out of the repo as before; handoff/config/ holds
example configs carrying the backend, edge and lowercase rules instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LrgrMhPG8CTo9XZF326rjZ
2026-10-10 17:06:34 -04:00

382 lines
14 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)"
# 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 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=""
# Reading the ruleset needs root, so don't probe it on a dry run.
(( 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 "$@"