Profiles: machines, roles and shared layers
The repo was one machine's capture. It is now a set of profiles built from layers under profiles/: machines/ hp-laptop, macbook-air-m2, coder-desktop roles/ developer, devops, ai-agents features/ handoff (lan-mouse) shared base, personal, personal-desktop A profile resolves to its includes (depth first), any --roles, then itself; for a path several layers provide, the last wins. install.sh --show prints the layers and which one owns each file. Hyprland settings no longer share a file: base's hyprland.lua loads ~/.config/hypr/layers/*.lua with Omarchy's own require_all, and bindings.lua stays stock so nothing binds twice. Verified with Hyprland 0.56.2 --verify-config for all three profiles, plus a broken probe layer to prove the directory is loaded. packages.txt gains per-line arch (Arch Linux ARM lacks opencode, codex, mise, shellcheck) and cmd= (skip when dotfiles' ~/.local/bin already provides the command), and an [ignore] section that replaces capture.sh's hard-coded list. Tab-separated records would have collapsed empty fields under read, so package records use the unit separator. hp-laptop: re-captured. monitors.lua catch-all scale is now 1, shell.json has four new plugins, fix-stuck-displays has its connector-name fix, navbar-cat is gone, plugin-manager and omarchy-phone are pinned, the herdr-hud working tree is re-diffed and bin/herdr-web rides along. Its handoff peer is now the Air (192.168.1.49); the old default list was the HP's own addresses, written from the Air's side. macbook-air-m2: captured read-only over SSH as it is (no themes, plugins or bar changes). Its LAN_MOUSE_POINTER_SCALE=0.7 moves from the shared unit into a lan-mouse.service.d drop-in in its own layer. coder-desktop: the Omarchy settings covert-cloud's image used to patch in by hand (Virtual-1 at 1920x1080, no animations, stay-awake, Retro 82 once), for the image to apply at a pinned commit. Roles reach it per workspace via install.sh --roles ... --no-clobber. capture.sh writes each file back into the layer that owns it, and refuses to run before the first layered install unless --force, since the old file shapes would land in shared layers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0153egowV1Xw2RNjZnSyZicc
This commit is contained in:
+353
-162
@@ -1,28 +1,38 @@
|
||||
#!/bin/bash
|
||||
# Apply this repo's Omarchy customizations to the machine it runs on.
|
||||
# Apply an omarchy-setup profile 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
|
||||
# ./install.sh # this machine's profile, every step
|
||||
# ./install.sh --profile hp-laptop # pick the profile
|
||||
# ./install.sh --roles devops,ai-agents # add roles on top of the profile
|
||||
# ./install.sh config themes # only the named steps
|
||||
# ./install.sh --dry-run # print what would change, touch nothing
|
||||
# ./install.sh --no-clobber # never overwrite a file that differs
|
||||
# ./install.sh --show # the layers, and which layer owns each file
|
||||
# ./install.sh --profiles | --list # list profiles | steps
|
||||
#
|
||||
# Steps: packages config themes plugins bin services system
|
||||
# Exits non-zero if any step reported an error (x).
|
||||
#
|
||||
# With no --profile it uses the one the last install recorded in
|
||||
# ~/.config/omarchy-setup/profile, else the machine whose hostnames= matches.
|
||||
#
|
||||
# Steps: packages config themes plugins bin handoff services system setup
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# Omarchy's commands (omarchy-theme-set, omarchy-pkg-add) are only on PATH in a
|
||||
# login session; an image build over SSH or a Coder start script has neither.
|
||||
OMARCHY_PATH=${OMARCHY_PATH:-/usr/share/omarchy}
|
||||
export OMARCHY_PATH
|
||||
case ":$PATH:" in *":$OMARCHY_PATH/bin:"*) ;; *) [[ -d $OMARCHY_PATH/bin ]] && PATH="$PATH:$OMARCHY_PATH/bin" ;; esac
|
||||
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
|
||||
NO_CLOBBER=0
|
||||
FORCE=0
|
||||
STEPS=(packages config themes plugins bin handoff services system setup)
|
||||
|
||||
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=; }
|
||||
@@ -32,10 +42,14 @@ 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"; }
|
||||
ERRORS=0
|
||||
err() { printf ' %sx %s%s\n' "$c_err" "$*" "$c_off"; ERRORS=$((ERRORS + 1)); }
|
||||
|
||||
run() { if (( DRY )); then printf ' %s? would: %s%s\n' "$c_skip" "$*" "$c_off"; else "$@"; fi; }
|
||||
|
||||
# shellcheck source=lib/layers.sh
|
||||
source "$REPO/lib/layers.sh"
|
||||
|
||||
# install_file <src> <dest> [mode] -- copy only when content differs, backing up first
|
||||
install_file() {
|
||||
local src=$1 dest=$2 mode=${3:-}
|
||||
@@ -44,6 +58,10 @@ install_file() {
|
||||
skip "$rel"
|
||||
return 0
|
||||
fi
|
||||
if [[ -e $dest ]] && (( NO_CLOBBER )); then
|
||||
warn "$rel differs from the profile - kept (--no-clobber)"
|
||||
return 0
|
||||
fi
|
||||
if (( DRY )); then
|
||||
printf ' %s? would write %s%s\n' "$c_skip" "$rel" "$c_off"
|
||||
return 0
|
||||
@@ -61,77 +79,110 @@ install_file() {
|
||||
fi
|
||||
}
|
||||
|
||||
# hypr_reload -- reload Hyprland if one is running, even from outside the
|
||||
# session (a Coder workspace-start script has no HYPRLAND_INSTANCE_SIGNATURE).
|
||||
hypr_reload() {
|
||||
(( DRY )) && return 0
|
||||
command -v hyprctl >/dev/null || return 0
|
||||
local sig=${HYPRLAND_INSTANCE_SIGNATURE:-}
|
||||
if [[ -z $sig ]]; then
|
||||
sig=$(hyprctl instances -j 2>/dev/null | jq -r '.[0].instance // empty' 2>/dev/null)
|
||||
fi
|
||||
[[ -n $sig ]] || return 0
|
||||
HYPRLAND_INSTANCE_SIGNATURE=$sig hyprctl reload >/dev/null 2>&1
|
||||
local errs; errs=$(HYPRLAND_INSTANCE_SIGNATURE=$sig 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
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- packages
|
||||
step_packages() {
|
||||
head_ "Packages"
|
||||
command -v omarchy >/dev/null || { err "omarchy not on PATH - is this an Omarchy system?"; return 1; }
|
||||
load_packages
|
||||
|
||||
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
|
||||
local e name archs cmds layer missing=() present=0 bycmd=() offarch=0
|
||||
for e in "${PKG_repo[@]}"; do
|
||||
IFS=$PKG_FS read -r name archs cmds layer <<<"$e"
|
||||
if ! pkg_applies "$archs"; then (( offarch++ )); continue; fi
|
||||
if pacman -Qq "$name" &>/dev/null; then (( present++ ))
|
||||
elif pkg_cmd_present "$cmds"; then bycmd+=("$name")
|
||||
else missing+=("$name"); fi
|
||||
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"
|
||||
skip "all $present repo packages present"
|
||||
fi
|
||||
(( ${#bycmd[@]} )) && skip "already on PATH from elsewhere, not installed: ${bycmd[*]}"
|
||||
(( offarch )) && skip "$offarch not built for $(uname -m), skipped"
|
||||
|
||||
missing=()
|
||||
for p in "${aur[@]}"; do pacman -Qq "$p" &>/dev/null || missing+=("$p"); done
|
||||
for e in "${PKG_aur[@]}"; do
|
||||
IFS=$PKG_FS read -r name archs cmds layer <<<"$e"
|
||||
pkg_applies "$archs" || continue
|
||||
pacman -Qq "$name" &>/dev/null || pkg_cmd_present "$cmds" || missing+=("$name")
|
||||
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"
|
||||
skip "all ${#PKG_aur[@]} AUR packages present"
|
||||
fi
|
||||
|
||||
for p in "${optional[@]}"; do
|
||||
pacman -Qq "$p" &>/dev/null || warn "optional, not installed: $p (omarchy pkg add $p)"
|
||||
for e in "${PKG_optional[@]}"; do
|
||||
IFS=$PKG_FS read -r name archs cmds layer <<<"$e"
|
||||
pkg_applies "$archs" || continue
|
||||
pacman -Qq "$name" &>/dev/null || warn "optional, not installed: $name (omarchy pkg add $name)"
|
||||
done
|
||||
|
||||
if (( ${#P_DOTFILES[@]} )) && [[ ! -d $HOME/.dotfiles ]]; then
|
||||
local -A u=(); local m; for m in "${P_DOTFILES[@]}"; do u[$m]=1; done
|
||||
say ""
|
||||
say " This profile's roles pair with dotfiles modules: ${!u[*]}"
|
||||
say " (git.nic-oconnor.com/public/dotfiles) - not run from here."
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- 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)
|
||||
local rel layer
|
||||
declare -A mine=()
|
||||
while IFS=$'\t' read -r rel layer; do
|
||||
install_file "$PROFILES/$layer/config/$rel" "$HOME/.config/$rel"
|
||||
mine[$rel]=1
|
||||
done < <(owners 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
|
||||
# A hypr/layers/ file that no layer of this profile provides is left over from
|
||||
# a role or profile that no longer applies. It still loads, so say so.
|
||||
local f
|
||||
for f in "$HOME"/.config/hypr/layers/*.lua; do
|
||||
[[ -e $f ]] || continue
|
||||
[[ -n ${mine[hypr/layers/${f##*/}]:-} ]] && continue
|
||||
warn "~/.config/hypr/layers/${f##*/} is from no layer of this profile - still loaded; rm it if unwanted"
|
||||
done
|
||||
|
||||
hypr_reload
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- themes
|
||||
step_themes() {
|
||||
head_ "Themes -> ~/.config/omarchy/themes"
|
||||
local t name dest
|
||||
for t in "$REPO"/themes/*/; do
|
||||
[[ -d $t ]] || continue
|
||||
name=$(basename -- "$t")
|
||||
local l t name dest
|
||||
declare -A theme=()
|
||||
for l in "${LAYERS[@]}"; do
|
||||
for t in "$PROFILES/$l"/themes/*/; do
|
||||
[[ -d $t ]] && theme[$(basename -- "$t")]=$t
|
||||
done
|
||||
done
|
||||
(( ${#theme[@]} )) || skip "no layer carries themes"
|
||||
for name in $(printf '%s\n' "${!theme[@]}" | sort); do
|
||||
t=${theme[$name]}
|
||||
dest="$HOME/.config/omarchy/themes/$name"
|
||||
if [[ -d $dest ]] && diff -rq "$t" "$dest" >/dev/null 2>&1; then
|
||||
skip "$name"
|
||||
@@ -141,77 +192,103 @@ step_themes() {
|
||||
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>"
|
||||
if (( ${#P_AETHER[@]} )); then
|
||||
say ""
|
||||
say " Themes managed by aether (not in this repo - reinstall with \`aether\`):"
|
||||
say " ${P_AETHER[*]}"
|
||||
fi
|
||||
(( ${#theme[@]} )) && say " Apply one with: omarchy theme set <name>"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- plugins
|
||||
step_plugins() {
|
||||
head_ "Shell plugins -> ~/.config/omarchy/plugins"
|
||||
local dir="$HOME/.config/omarchy/plugins"
|
||||
local dir="$HOME/.config/omarchy/plugins" l
|
||||
declare -A url=() sha=()
|
||||
local id u s
|
||||
for l in "${LAYERS[@]}"; do
|
||||
[[ -f $PROFILES/$l/plugins.txt ]] || continue
|
||||
while IFS=$'\t' read -r id u s; do
|
||||
[[ -z ${id:-} || $id == \#* ]] && continue
|
||||
url[$id]=$u; sha[$id]=$s
|
||||
done < "$PROFILES/$l/plugins.txt"
|
||||
done
|
||||
if (( ! ${#url[@]} )); then skip "no layer carries plugins"; return 0; fi
|
||||
[[ -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"
|
||||
local p
|
||||
for id in $(printf '%s\n' "${!url[@]}" | sort); do
|
||||
p="$dir/$id"; s=${sha[$id]}
|
||||
if [[ -d $p/.git ]]; then
|
||||
if [[ $(git -C "$p" rev-parse HEAD 2>/dev/null) == "$sha" ]]; then
|
||||
skip "$id @ ${sha:0:7}"
|
||||
if [[ $(git -C "$p" rev-parse HEAD 2>/dev/null) == "$s" ]]; then
|
||||
skip "$id @ ${s: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})"
|
||||
warn "$id has local changes - leaving it alone (wanted ${s:0:7})"
|
||||
continue
|
||||
fi
|
||||
run git -C "$p" fetch --quiet origin
|
||||
run git -C "$p" checkout --quiet "$sha" && ok "$id -> ${sha:0:7}"
|
||||
run git -C "$p" checkout --quiet "$s" && ok "$id -> ${s:0:7}"
|
||||
else
|
||||
if (( DRY )); then
|
||||
printf ' %s? would clone %s -> %s%s\n' "$c_skip" "$id" "${sha:0:7}" "$c_off"
|
||||
printf ' %s? would clone %s -> %s%s\n' "$c_skip" "$id" "${s: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}"
|
||||
git clone --quiet "${url[$id]}" "$p" || { err "clone failed: $id (${url[$id]})"; continue; }
|
||||
git -C "$p" checkout --quiet "$s" || warn "$id: sha $s not found, left on default branch"
|
||||
ok "$id @ ${s:0:7}"
|
||||
fi
|
||||
done < "$REPO/plugins.txt"
|
||||
done
|
||||
|
||||
apply_herdr_hud
|
||||
apply_worktree_patch io.github.tallsam.navbar-cat
|
||||
# Local plugin work that exists on no remote, per layer:
|
||||
# <id>.bundle a 'local' branch, replayed and checked out
|
||||
# <id>-worktree.patch uncommitted working-tree changes
|
||||
# <id>--bin--<file> an untracked file installed as <plugin>/bin/<file>
|
||||
local f
|
||||
for l in "${LAYERS[@]}"; do
|
||||
[[ -d $PROFILES/$l/plugin-patches ]] || continue
|
||||
for f in "$PROFILES/$l"/plugin-patches/*.bundle; do
|
||||
[[ -f $f ]] && apply_bundle "$(basename -- "$f" .bundle)" "$f"
|
||||
done
|
||||
for f in "$PROFILES/$l"/plugin-patches/*-worktree.patch; do
|
||||
[[ -f $f ]] || continue
|
||||
id=$(basename -- "$f"); apply_worktree_patch "${id%-worktree.patch}" "$f"
|
||||
done
|
||||
for f in "$PROFILES/$l"/plugin-patches/*--bin--*; do
|
||||
[[ -f $f ]] || continue
|
||||
id=$(basename -- "$f")
|
||||
[[ -d $dir/${id%%--bin--*}/.git ]] || continue
|
||||
install_file "$f" "$dir/${id%%--bin--*}/bin/${id##*--bin--}" 755
|
||||
done
|
||||
done
|
||||
|
||||
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."
|
||||
say " Bar placement lives in config/omarchy/shell.json, installed by the config"
|
||||
say " 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
|
||||
|
||||
# apply_bundle <id> <bundle> -- replay a plugin's 'local' branch. indie.herdr-hud
|
||||
# carries commits that intentionally never went to the upstream PR.
|
||||
apply_bundle() {
|
||||
local id=$1 bundle=$2
|
||||
local p="$HOME/.config/omarchy/plugins/$id"
|
||||
[[ -d $p/.git ]] || return 0
|
||||
if git -C "$p" rev-parse --verify --quiet local >/dev/null; then
|
||||
skip "indie.herdr-hud: 'local' branch already present"
|
||||
skip "$id: 'local' branch already present"
|
||||
elif (( DRY )); then
|
||||
printf ' %s? would replay the herdr-hud local branch%s\n' "$c_skip" "$c_off"
|
||||
return 0
|
||||
printf ' %s? would replay the %s local branch%s\n' "$c_skip" "$id" "$c_off"
|
||||
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"
|
||||
&& { git -C "$p" checkout --quiet local && ok "$id: replayed 'local' branch"; } \
|
||||
|| warn "$id: 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 id=$1 patch=$2
|
||||
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"
|
||||
@@ -227,52 +304,74 @@ apply_worktree_patch() {
|
||||
# ---------------------------------------------------------------- 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
|
||||
local rel layer n=0
|
||||
while IFS=$'\t' read -r rel layer; do
|
||||
install_file "$PROFILES/$layer/bin/$rel" "$HOME/.local/bin/$rel" 755
|
||||
(( n++ ))
|
||||
done < <(owners bin)
|
||||
(( n )) || { skip "no layer carries scripts"; return 0; }
|
||||
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."
|
||||
[[ -n $(layers_with bin/fix-stuck-displays) ]] && {
|
||||
say ""
|
||||
say " fix-stuck-displays needs passwordless sudo for 'tee /sys/class/drm/*/status'."
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- services
|
||||
# install_unit <name.service> -- the unit plus its <name>.service.d/*.conf
|
||||
# drop-ins, from whichever layers own them. Sets UNITS_CHANGED.
|
||||
UNITS_CHANGED=0
|
||||
install_unit() {
|
||||
local name=$1 rel layer dest
|
||||
while IFS=$'\t' read -r rel layer; do
|
||||
[[ $rel == "$name" || $rel == "$name.d/"* ]] || continue
|
||||
dest="$HOME/.config/systemd/user/$rel"
|
||||
[[ -e $dest ]] && cmp -s "$PROFILES/$layer/systemd/user/$rel" "$dest" || UNITS_CHANGED=1
|
||||
install_file "$PROFILES/$layer/systemd/user/$rel" "$dest" 0644
|
||||
done < <(owners systemd/user)
|
||||
}
|
||||
|
||||
# enable_unit <name.service> -- systemctl --user enable, or, with no user
|
||||
# manager to talk to (an image build), the symlink enable would have made.
|
||||
enable_unit() {
|
||||
local name=$1 want
|
||||
if systemctl --user is-enabled --quiet "$name" 2>/dev/null; then
|
||||
skip "$name already enabled"; return 0
|
||||
fi
|
||||
if (( DRY )); then printf ' %s? would enable %s%s\n' "$c_skip" "$name" "$c_off"; return 0; fi
|
||||
if systemctl --user enable "$name" >/dev/null 2>&1; then ok "enabled $name"; return 0; fi
|
||||
want=$(sed -n 's/^WantedBy=//p' "$HOME/.config/systemd/user/$name" | head -1)
|
||||
[[ -n $want ]] || { err "could not enable $name"; return 1; }
|
||||
mkdir -p "$HOME/.config/systemd/user/$want.wants"
|
||||
ln -sf "../$name" "$HOME/.config/systemd/user/$want.wants/$name" && ok "enabled $name (symlink; no user manager)"
|
||||
}
|
||||
|
||||
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
|
||||
local rel layer names=()
|
||||
while IFS=$'\t' read -r rel layer; do
|
||||
[[ $rel == *.service ]] && names+=("$rel")
|
||||
done < <(owners systemd/user)
|
||||
(( ${#names[@]} )) || { skip "no layer carries units"; return 0; }
|
||||
UNITS_CHANGED=0
|
||||
for rel in "${names[@]}"; do install_unit "$rel"; done
|
||||
(( UNITS_CHANGED )) && run systemctl --user daemon-reload 2>/dev/null
|
||||
for rel in "${names[@]}"; do enable_unit "$rel"; done
|
||||
say ""
|
||||
say " Both are WantedBy=graphical-session.target, so they start with Hyprland"
|
||||
say " Units 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)"
|
||||
local layer; layer=$(layers_with handoff | tail -1)
|
||||
[[ -n $layer ]] || { skip "not part of this profile"; return 0; }
|
||||
local hd="$PROFILES/$layer/handoff"
|
||||
|
||||
# Self-sufficient on purpose: `install.sh handoff` alone brings input sharing up
|
||||
# on an otherwise stock machine, without the packages/config/themes steps that
|
||||
@@ -280,8 +379,8 @@ step_handoff() {
|
||||
# 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.
|
||||
# NOT python-evdev: 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")
|
||||
@@ -293,11 +392,11 @@ step_handoff() {
|
||||
skip "lan-mouse, rust"
|
||||
fi
|
||||
|
||||
install_file "$REPO/bin/omarchy-handoff" "$HOME/.local/bin/omarchy-handoff" 0755
|
||||
install_file "$PROFILES/$layer/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.
|
||||
# 1. The patched daemon. lan-mouse.service points at this path, so it has to
|
||||
# exist before the unit is enabled. Per-architecture: the HP 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
|
||||
@@ -306,27 +405,27 @@ step_handoff() {
|
||||
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"
|
||||
if "$hd/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
|
||||
# 2. The unit (and this machine's drop-ins, e.g. the Air's pointer scale).
|
||||
UNITS_CHANGED=0
|
||||
install_unit lan-mouse.service
|
||||
(( UNITS_CHANGED )) && run systemctl --user daemon-reload 2>/dev/null
|
||||
enable_unit lan-mouse.service
|
||||
|
||||
# 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.
|
||||
local ips=("${P_HANDOFF_IPS[@]}")
|
||||
[[ -n ${HANDOFF_PEER_IPS:-} ]] && read -ra ips <<<"$HANDOFF_PEER_IPS"
|
||||
if ! command -v ufw >/dev/null; then
|
||||
skip "ufw not installed - nothing to open"
|
||||
elif (( ! ${#ips[@]} )); then
|
||||
warn "no handoff_peer_ips in this profile - set them, or HANDOFF_PEER_IPS='a b'"
|
||||
else
|
||||
local ip rules=""
|
||||
# Both reading and changing the ruleset need root. If sudo can't run without
|
||||
@@ -334,7 +433,7 @@ step_handoff() {
|
||||
# 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
|
||||
for ip in "${ips[@]}"; do
|
||||
say " sudo ufw allow from $ip to any port 4242 proto udp comment 'lan-mouse peer $ip'"
|
||||
done
|
||||
say ""
|
||||
@@ -343,7 +442,7 @@ step_handoff() {
|
||||
return 0
|
||||
fi
|
||||
(( DRY )) || rules=$(sudo ufw status 2>/dev/null || true)
|
||||
for ip in "${HANDOFF_PEER_IPS[@]}"; do
|
||||
for ip in "${ips[@]}"; do
|
||||
if [[ $rules == *"4242"*"$ip"* ]]; then
|
||||
skip "ufw allows 4242/udp from $ip"
|
||||
elif (( DRY )); then
|
||||
@@ -359,7 +458,7 @@ step_handoff() {
|
||||
|
||||
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 " fingerprints). Start from $layer/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"
|
||||
@@ -369,12 +468,12 @@ step_handoff() {
|
||||
|
||||
# ---------------------------------------------------------------- system
|
||||
step_system() {
|
||||
head_ "System drop-ins (needs sudo)"
|
||||
local src rel dest
|
||||
while IFS= read -r -d '' src; do
|
||||
rel=${src#"$REPO/system/"}
|
||||
head_ "System drop-ins -> /etc (needs sudo)"
|
||||
local rel layer dest n=0 sysctl=0 modprobe=0
|
||||
while IFS=$'\t' read -r rel layer; do
|
||||
(( n++ ))
|
||||
dest="/etc/$rel"
|
||||
if [[ -e $dest ]] && cmp -s "$src" "$dest"; then
|
||||
if [[ -e $dest ]] && cmp -s "$PROFILES/$layer/system/$rel" "$dest"; then
|
||||
skip "$dest"
|
||||
continue
|
||||
fi
|
||||
@@ -382,27 +481,85 @@ step_system() {
|
||||
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)
|
||||
sudo install -D -m 0644 "$PROFILES/$layer/system/$rel" "$dest" && ok "$dest"
|
||||
[[ $rel == sysctl.d/* ]] && sysctl=1
|
||||
[[ $rel == modprobe.d/* ]] && modprobe=1
|
||||
done < <(owners system)
|
||||
(( n )) || { skip "no layer carries /etc drop-ins"; return 0; }
|
||||
|
||||
if (( ! DRY )); then
|
||||
sudo sysctl --system >/dev/null 2>&1 && ok "sysctl reloaded"
|
||||
(( sysctl )) && sudo sysctl --system >/dev/null 2>&1 && ok "sysctl reloaded"
|
||||
if (( modprobe )); then
|
||||
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"
|
||||
say " modprobe.d options (hid_apple fnmode, appledrm notch) only take effect once"
|
||||
say " the initramfs is rebuilt or the module reloads:"
|
||||
say " sudo mkinitcpio -P # or: sudo modprobe -r <module> && sudo modprobe <module>"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- setup
|
||||
# Each layer's setup.sh, for state outside ~/.config (markers, gsettings, a
|
||||
# first-run theme). Run last, in layer order.
|
||||
step_setup() {
|
||||
head_ "Layer setup scripts"
|
||||
local l n=0
|
||||
for l in "${LAYERS[@]}"; do
|
||||
[[ -x $PROFILES/$l/setup.sh ]] || continue
|
||||
(( n++ ))
|
||||
say " $l/setup.sh"
|
||||
OMARCHY_SETUP_DRY=$DRY OMARCHY_SETUP_REPO=$REPO OMARCHY_SETUP_LAYER="$PROFILES/$l" \
|
||||
"$PROFILES/$l/setup.sh" || err "$l/setup.sh failed"
|
||||
done
|
||||
(( n )) || skip "no layer has a setup.sh"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- show
|
||||
show() {
|
||||
say "${c_hd}$PROFILE${c_off} $(layer_description "$PROFILE")"
|
||||
[[ -n $P_ARCH ]] && say " arch: $P_ARCH"
|
||||
say ""
|
||||
say "Layers, in order (later wins):"
|
||||
local l; for l in "${LAYERS[@]}"; do printf ' %-26s %s\n' "$l" "$(layer_description "$l")"; done
|
||||
local sub rel layer
|
||||
for sub in config bin systemd/user system; do
|
||||
local any=0
|
||||
while IFS=$'\t' read -r rel layer; do
|
||||
(( any )) || { say ""; say "$sub/"; any=1; }
|
||||
printf ' %-52s %s\n' "$rel" "$layer"
|
||||
done < <(owners "$sub")
|
||||
done
|
||||
}
|
||||
|
||||
list_profiles() {
|
||||
local kind d
|
||||
for kind in machines roles features; do
|
||||
say "${c_hd}$kind${c_off}"
|
||||
for d in "$PROFILES/$kind"/*/; do
|
||||
d=${d%/}; printf ' %-18s %s\n' "${d##*/}" "$(layer_description "$kind/${d##*/}")"
|
||||
done
|
||||
done
|
||||
say "${c_hd}shared${c_off}"
|
||||
for d in base personal personal-desktop; do printf ' %-18s %s\n' "$d" "$(layer_description "$d")"; done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- main
|
||||
main() {
|
||||
local want=()
|
||||
for a in "$@"; do
|
||||
local want=() profile="" roles_arg="" roles_given=0 show_only=0 a
|
||||
while (( $# )); do
|
||||
a=$1; shift
|
||||
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 ;;
|
||||
--dry-run|-n) DRY=1 ;;
|
||||
--no-clobber) NO_CLOBBER=1 ;;
|
||||
--force) FORCE=1 ;;
|
||||
--profile|-p) profile=${1:?--profile needs a name}; shift ;;
|
||||
--profile=*) profile=${a#*=} ;;
|
||||
--roles|-r) roles_arg=${1-}; roles_given=1; shift ;;
|
||||
--roles=*) roles_arg=${a#*=}; roles_given=1 ;;
|
||||
--show) show_only=1 ;;
|
||||
--profiles) list_profiles; exit 0 ;;
|
||||
--list|-l) printf '%s\n' "${STEPS[@]}"; exit 0 ;;
|
||||
-h|--help) awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "${BASH_SOURCE[0]}"; 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 ;;
|
||||
@@ -410,15 +567,49 @@ main() {
|
||||
done
|
||||
(( ${#want[@]} )) || want=("${STEPS[@]}")
|
||||
|
||||
say "${c_hd}omarchy-setup${c_off} repo=$REPO"
|
||||
local saved_profile="" saved_roles=""
|
||||
if def=$(default_profile); then IFS=$'\t' read -r saved_profile saved_roles <<<"$def"; fi
|
||||
if [[ -z $profile ]]; then
|
||||
profile=$saved_profile
|
||||
[[ -n $profile ]] || { err "no --profile given and none matches host $(hostname)"; list_profiles; exit 2; }
|
||||
fi
|
||||
local rp; rp=$(find_layer "$profile") || { err "no such profile: $profile"; list_profiles; exit 2; }
|
||||
# No --roles reuses the roles last installed with this profile; --roles ""
|
||||
# means none (a Coder workspace with every extra role deselected).
|
||||
(( roles_given )) || [[ $rp != "$saved_profile" ]] || roles_arg=$saved_roles
|
||||
ROLES=()
|
||||
[[ -n $roles_arg ]] && IFS=', ' read -ra ROLES <<<"$roles_arg"
|
||||
|
||||
resolve "$rp" "${ROLES[@]}" || exit 2
|
||||
load_settings
|
||||
|
||||
if (( show_only )); then show; exit 0; fi
|
||||
|
||||
say "${c_hd}omarchy-setup${c_off} profile=$PROFILE${ROLES:+ roles=${ROLES[*]}} repo=$REPO"
|
||||
say " layers: ${LAYERS[*]}"
|
||||
(( DRY )) && warn "dry run - nothing will be changed"
|
||||
(( NO_CLOBBER )) && warn "--no-clobber: files that differ from the profile are kept"
|
||||
command -v omarchy >/dev/null || warn "omarchy not found; this looks like a non-Omarchy host"
|
||||
|
||||
for s in "${want[@]}"; do "step_$s"; done
|
||||
if [[ -n $P_ARCH && $P_ARCH != "$(uname -m)" ]]; then
|
||||
err "$PROFILE is for $P_ARCH, this machine is $(uname -m)"
|
||||
(( FORCE )) || { say " --force to apply it anyway"; exit 2; }
|
||||
fi
|
||||
local host; host=$(hostname 2>/dev/null || cat /etc/hostname)
|
||||
if (( ${#P_HOSTNAMES[@]} )) && [[ " ${P_HOSTNAMES[*]} " != *" $host "* ]]; then
|
||||
warn "$PROFILE is for ${P_HOSTNAMES[*]}, this host is $host"
|
||||
fi
|
||||
|
||||
# Canonical order whatever order they were named in: setup after config, etc.
|
||||
for s in "${STEPS[@]}"; do [[ " ${want[*]} " == *" $s "* ]] && "step_$s"; done
|
||||
|
||||
(( DRY )) || save_state
|
||||
|
||||
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"
|
||||
# Non-zero when any step reported an error, so an image build fails on it.
|
||||
if (( ERRORS )); then err "$ERRORS error(s) above"; exit 1; fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
Reference in New Issue
Block a user