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:
+164
-107
@@ -2,11 +2,14 @@
|
||||
# Pull the live machine's state back into this repo - the reverse of install.sh.
|
||||
#
|
||||
# Run it after customizing something by hand, then review `git diff` and commit.
|
||||
# It only refreshes paths the repo already tracks; it never invents new ones, so
|
||||
# adding a newly customized file is a deliberate act (see README, "Adding a file").
|
||||
# Each live file goes back into the layer that OWNS it for this machine's
|
||||
# profile (the last layer providing that path; `./install.sh --show` lists
|
||||
# them). It only refreshes paths the repo already tracks, so adding a newly
|
||||
# customized file is a deliberate act (see README, "Adding a file").
|
||||
#
|
||||
# ./capture.sh # refresh everything
|
||||
# ./capture.sh config # only the named steps
|
||||
# ./capture.sh # this machine's profile, every step
|
||||
# ./capture.sh --profile hp-laptop # name it explicitly
|
||||
# ./capture.sh config # only the named steps
|
||||
# ./capture.sh --list
|
||||
#
|
||||
# Steps: config themes plugins bin services system packages
|
||||
@@ -16,15 +19,19 @@ set -uo pipefail
|
||||
REPO="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
STEPS=(config themes plugins bin services system packages)
|
||||
|
||||
c_ch=$'\033[33m'; c_skip=$'\033[2m'; c_warn=$'\033[33m'; c_hd=$'\033[1m'; c_off=$'\033[0m'
|
||||
[[ -t 1 ]] || { c_ch=; c_skip=; c_warn=; c_hd=; c_off=; }
|
||||
c_ch=$'\033[33m'; c_skip=$'\033[2m'; c_warn=$'\033[33m'; c_err=$'\033[31m'; c_hd=$'\033[1m'; c_off=$'\033[0m'
|
||||
[[ -t 1 ]] || { c_ch=; 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"; }
|
||||
chg() { printf ' %sM %s%s\n' "$c_ch" "$*" "$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"; }
|
||||
gone() { printf ' %s? %s%s\n' "$c_warn" "$*" "$c_off"; }
|
||||
|
||||
# shellcheck source=lib/layers.sh
|
||||
source "$REPO/lib/layers.sh"
|
||||
|
||||
# pull_back <live> <repo-copy> <label>
|
||||
pull_back() {
|
||||
local live=$1 dest=$2 label=$3
|
||||
@@ -32,121 +39,139 @@ pull_back() {
|
||||
if cmp -s "$live" "$dest"; then skip "$label"; else cp "$live" "$dest"; chg "$label"; fi
|
||||
}
|
||||
|
||||
step_config() {
|
||||
head_ "~/.config -> config/"
|
||||
local dest rel
|
||||
while IFS= read -r -d '' dest; do
|
||||
rel=${dest#"$REPO/config/"}
|
||||
pull_back "$HOME/.config/$rel" "$dest" "$rel"
|
||||
done < <(find "$REPO/config" -type f -print0 | sort -z)
|
||||
# pull_tree <subdir> <live-root> -- every owned file under <layer>/<subdir>
|
||||
pull_tree() {
|
||||
local sub=$1 root=$2 rel layer
|
||||
while IFS=$'\t' read -r rel layer; do
|
||||
pull_back "$root/$rel" "$PROFILES/$layer/$sub/$rel" "$rel [$layer]"
|
||||
done < <(owners "$sub")
|
||||
}
|
||||
|
||||
step_config() { head_ "~/.config -> layers' config/"; pull_tree config "$HOME/.config"; }
|
||||
step_bin() { head_ "~/.local/bin -> layers' bin/"; pull_tree bin "$HOME/.local/bin"; }
|
||||
step_services() { head_ "systemd user units -> layers' systemd/"; pull_tree systemd/user "$HOME/.config/systemd/user"; }
|
||||
step_system() { head_ "/etc drop-ins -> layers' system/"; pull_tree system /etc; }
|
||||
|
||||
step_themes() {
|
||||
head_ "Themes"
|
||||
local t name live
|
||||
for t in "$REPO"/themes/*/; do
|
||||
[[ -d $t ]] || continue
|
||||
name=$(basename -- "$t")
|
||||
local l t name live
|
||||
declare -A theme=()
|
||||
for l in "${LAYERS[@]}"; do
|
||||
for t in "$PROFILES/$l"/themes/*/; do [[ -d $t ]] && theme[$(basename -- "$t")]=$l; done
|
||||
done
|
||||
for name in $(printf '%s\n' "${!theme[@]}" | sort); do
|
||||
t="$PROFILES/${theme[$name]}/themes/$name/"
|
||||
live="$HOME/.config/omarchy/themes/$name"
|
||||
if [[ ! -d $live ]]; then gone "$name - no longer installed"; continue; fi
|
||||
if diff -rq "$live" "$t" >/dev/null 2>&1; then
|
||||
skip "$name"
|
||||
skip "$name [${theme[$name]}]"
|
||||
else
|
||||
rm -rf "$t"; mkdir -p "$t"; cp -r "$live/." "$t/"
|
||||
chg "$name"
|
||||
chg "$name [${theme[$name]}]"
|
||||
fi
|
||||
done
|
||||
# Flag themes installed by hand that the repo does not carry.
|
||||
local l lname
|
||||
# Flag themes installed by hand that no layer carries.
|
||||
local lname
|
||||
for l in "$HOME"/.config/omarchy/themes/*/; do
|
||||
[[ -d $l ]] || continue
|
||||
lname=$(basename -- "$l")
|
||||
[[ -d "$REPO/themes/$lname" ]] && continue
|
||||
[[ -n ${theme[$lname]:-} ]] && continue
|
||||
[[ -e "$l/.aether-managed" ]] && continue
|
||||
warn "$lname is installed but not in this repo (cp -r it into themes/ to track it)"
|
||||
[[ " ${P_AETHER[*]} " == *" $lname "* ]] && continue
|
||||
warn "$lname is installed but in no layer (cp -r it into a layer's themes/ to track it)"
|
||||
done
|
||||
}
|
||||
|
||||
# Re-pin every plugin a layer lists to its live sha, drop the ones that are gone,
|
||||
# and add live plugins no layer lists to the last layer that has a plugins.txt.
|
||||
step_plugins() {
|
||||
head_ "Plugin pins and local work"
|
||||
local dir="$HOME/.config/omarchy/plugins"
|
||||
local tmp; tmp=$(mktemp)
|
||||
{
|
||||
echo "# Omarchy shell plugins. Fields: id<TAB>repo<TAB>pinned-sha"
|
||||
echo "# install.sh clones each into ~/.config/omarchy/plugins/<id> and checks out the sha."
|
||||
local p id url sha
|
||||
for p in "$dir"/*/; do
|
||||
[[ -d $p/.git ]] || continue
|
||||
id=$(basename -- "$p")
|
||||
url=$(git -C "$p" remote get-url origin 2>/dev/null) || continue
|
||||
sha=$(git -C "$p" rev-parse HEAD)
|
||||
printf '%s\t%s\t%s\n' "$id" "$url" "$sha"
|
||||
done
|
||||
} > "$tmp"
|
||||
if cmp -s "$tmp" "$REPO/plugins.txt"; then skip "plugins.txt"; else mv "$tmp" "$REPO/plugins.txt"; chg "plugins.txt"; fi
|
||||
rm -f "$tmp"
|
||||
local dir="$HOME/.config/omarchy/plugins" l f
|
||||
local files=()
|
||||
while IFS= read -r l; do files+=("$l"); done < <(layers_with plugins.txt)
|
||||
if (( ! ${#files[@]} )); then skip "no layer carries plugins"; return 0; fi
|
||||
|
||||
# herdr-hud's local-only branch, re-bundled
|
||||
local p="$dir/indie.herdr-hud"
|
||||
if [[ -d $p/.git ]] && git -C "$p" rev-parse --verify --quiet local >/dev/null; then
|
||||
local b="$REPO/plugin-patches/indie.herdr-hud.bundle"
|
||||
git -C "$p" bundle create "$b.new" origin/main..local >/dev/null 2>&1
|
||||
# Bundles embed a timestamp-free pack but differ byte-wise; compare the tips instead.
|
||||
if [[ -f $b ]] && [[ $(git -C "$p" bundle list-heads "$b" 2>/dev/null) == $(git -C "$p" bundle list-heads "$b.new" 2>/dev/null) ]]; then
|
||||
rm -f "$b.new"; skip "indie.herdr-hud.bundle"
|
||||
else
|
||||
mv "$b.new" "$b"; chg "indie.herdr-hud.bundle"
|
||||
declare -A listed=()
|
||||
local id u s line tmp
|
||||
for l in "${files[@]}"; do
|
||||
f="$PROFILES/$l/plugins.txt"; tmp=$(mktemp)
|
||||
while IFS= read -r line; do
|
||||
if [[ -z $line || $line == \#* ]]; then printf '%s\n' "$line" >> "$tmp"; continue; fi
|
||||
IFS=$'\t' read -r id u s <<<"$line"
|
||||
listed[$id]=1
|
||||
if [[ ! -d $dir/$id/.git ]]; then
|
||||
gone "$id - no longer installed, dropped from $l/plugins.txt"
|
||||
continue
|
||||
fi
|
||||
printf '%s\t%s\t%s\n' "$id" "$u" "$(git -C "$dir/$id" rev-parse HEAD)" >> "$tmp"
|
||||
done < "$f"
|
||||
if [[ $l == "${files[-1]}" ]]; then
|
||||
local p
|
||||
for p in "$dir"/*/; do
|
||||
id=$(basename -- "$p")
|
||||
[[ -n ${listed[$id]:-} ]] && continue
|
||||
if [[ ! -d $p/.git ]]; then warn "$id is not a git checkout - not tracked"; continue; fi
|
||||
if ! u=$(git -C "$p" remote get-url origin 2>/dev/null); then
|
||||
warn "$id has no origin remote - push it somewhere before it can be pinned"; continue
|
||||
fi
|
||||
printf '%s\t%s\t%s\n' "$id" "$u" "$(git -C "$p" rev-parse HEAD)" >> "$tmp"
|
||||
chg "$id - new, added to $l/plugins.txt"
|
||||
done
|
||||
fi
|
||||
[[ -f $p/bin/herdr-agents ]] && pull_back "$p/bin/herdr-agents" \
|
||||
"$REPO/plugin-patches/indie.herdr-hud--bin--herdr-agents" "indie.herdr-hud bin/herdr-agents"
|
||||
fi
|
||||
if cmp -s "$tmp" "$f"; then skip "$l/plugins.txt"; else mv "$tmp" "$f"; chg "$l/plugins.txt"; fi
|
||||
rm -f "$tmp"
|
||||
done
|
||||
|
||||
local f id
|
||||
for f in "$REPO"/plugin-patches/*-worktree.patch; do
|
||||
[[ -f $f ]] || continue
|
||||
id=$(basename -- "$f"); id=${id%-worktree.patch}
|
||||
if [[ ! -d $dir/$id/.git ]]; then gone "$id - not installed"; continue; fi
|
||||
local tmp2; tmp2=$(mktemp)
|
||||
git -C "$dir/$id" diff > "$tmp2"
|
||||
if cmp -s "$tmp2" "$f"; then skip "$id-worktree.patch"; else mv "$tmp2" "$f"; chg "$id-worktree.patch"; fi
|
||||
rm -f "$tmp2"
|
||||
# Local plugin work, per layer: re-bundle 'local' branches, re-diff working
|
||||
# trees, pull back --bin-- files. See install.sh step_plugins.
|
||||
local b new p untracked
|
||||
for l in "${LAYERS[@]}"; do
|
||||
[[ -d $PROFILES/$l/plugin-patches ]] || continue
|
||||
for b in "$PROFILES/$l"/plugin-patches/*.bundle; do
|
||||
[[ -f $b ]] || continue
|
||||
id=$(basename -- "$b" .bundle); p="$dir/$id"
|
||||
[[ -d $p/.git ]] && git -C "$p" rev-parse --verify --quiet local >/dev/null || { gone "$id: no 'local' branch here"; continue; }
|
||||
new="$b.new"
|
||||
git -C "$p" bundle create "$new" origin/main..local >/dev/null 2>&1
|
||||
# Bundles differ byte-wise run to run; compare the tips instead.
|
||||
if [[ $(git -C "$p" bundle list-heads "$b" 2>/dev/null) == $(git -C "$p" bundle list-heads "$new" 2>/dev/null) ]]; then
|
||||
rm -f "$new"; skip "$id.bundle [$l]"
|
||||
else
|
||||
mv "$new" "$b"; chg "$id.bundle [$l]"
|
||||
fi
|
||||
done
|
||||
for f in "$PROFILES/$l"/plugin-patches/*-worktree.patch; do
|
||||
[[ -f $f ]] || continue
|
||||
id=$(basename -- "$f"); id=${id%-worktree.patch}; p="$dir/$id"
|
||||
if [[ ! -d $p/.git ]]; then gone "$id - not installed"; continue; fi
|
||||
tmp=$(mktemp)
|
||||
git -C "$p" diff > "$tmp"
|
||||
if cmp -s "$tmp" "$f"; then skip "$id-worktree.patch [$l]"; else mv "$tmp" "$f"; chg "$id-worktree.patch [$l]"; fi
|
||||
rm -f "$tmp"
|
||||
# Untracked files are not in a diff; only --bin-- files carry them.
|
||||
while IFS= read -r untracked; do
|
||||
[[ $untracked == bin/* && -f $PROFILES/$l/plugin-patches/$id--bin--${untracked#bin/} ]] && continue
|
||||
warn "$id: untracked $untracked is not captured (bin/ files: cp to plugin-patches/$id--bin--<name>)"
|
||||
done < <(git -C "$p" ls-files --others --exclude-standard)
|
||||
done
|
||||
for f in "$PROFILES/$l"/plugin-patches/*--bin--*; do
|
||||
[[ -f $f ]] || continue
|
||||
id=$(basename -- "$f")
|
||||
pull_back "$dir/${id%%--bin--*}/bin/${id##*--bin--}" "$f" "${id%%--bin--*} bin/${id##*--bin--} [$l]"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
step_bin() {
|
||||
head_ "~/.local/bin -> bin/"
|
||||
local f
|
||||
for f in "$REPO"/bin/*; do
|
||||
[[ -f $f ]] || continue
|
||||
pull_back "$HOME/.local/bin/$(basename -- "$f")" "$f" "$(basename -- "$f")"
|
||||
done
|
||||
}
|
||||
|
||||
step_services() {
|
||||
head_ "systemd user units"
|
||||
local f name
|
||||
for f in "$REPO"/systemd/user/*.service; do
|
||||
[[ -f $f ]] || continue
|
||||
name=$(basename -- "$f")
|
||||
pull_back "$HOME/.config/systemd/user/$name" "$f" "$name"
|
||||
done
|
||||
}
|
||||
|
||||
step_system() {
|
||||
head_ "/etc drop-ins"
|
||||
local dest rel
|
||||
while IFS= read -r -d '' dest; do
|
||||
rel=${dest#"$REPO/system/"}
|
||||
pull_back "/etc/$rel" "$dest" "/etc/$rel"
|
||||
done < <(find "$REPO/system" -type f -print0 | sort -z)
|
||||
}
|
||||
|
||||
# Report drift in the package delta without rewriting the hand-annotated file:
|
||||
# packages.txt carries a "why" comment per line that a regenerated list would lose.
|
||||
# Report drift in the package delta without rewriting the hand-annotated files:
|
||||
# each line carries a "why" comment that a regenerated list would lose.
|
||||
step_packages() {
|
||||
head_ "Package delta (report only)"
|
||||
load_packages
|
||||
local listed; listed=$(mktemp)
|
||||
sed 's/#.*//' "$REPO/packages.txt" | tr -d '[:blank:]' | grep -v '^$' | grep -v '^\[' | sort -u > "$listed"
|
||||
local e name archs cmds layer
|
||||
for e in "${PKG_repo[@]}" "${PKG_aur[@]}" "${PKG_optional[@]}" "${PKG_ignore[@]}"; do
|
||||
IFS=$PKG_FS read -r name archs cmds layer <<<"$e"; printf '%s\n' "$name"
|
||||
done | sort -u > "$listed"
|
||||
|
||||
local baseline; baseline=$(mktemp)
|
||||
{
|
||||
@@ -155,32 +180,64 @@ step_packages() {
|
||||
cat /usr/share/omarchy/install/omarchy-base.packages 2>/dev/null
|
||||
} | grep -v '^$' | sort -u > "$baseline"
|
||||
|
||||
local extra; extra=$(mktemp)
|
||||
pacman -Qqe 2>/dev/null | sort -u | comm -23 - "$baseline" > "$extra"
|
||||
local untracked; untracked=$(pacman -Qqe 2>/dev/null | sort -u | comm -23 - "$baseline" | comm -23 - "$listed" | tr '\n' ' ')
|
||||
local missing=()
|
||||
for e in "${PKG_repo[@]}" "${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 [$layer]")
|
||||
done
|
||||
|
||||
local untracked; untracked=$(comm -23 "$extra" "$listed" | grep -vxE 'base|base-devel|sudo|omarchy|mkinitcpio|linux-firmware|efibootmgr|devtools|cmake|ninja|wayvnc|sunshine|freerdp|remmina|omarchy-remote-desktop-backend|pipewire-debug' | tr '\n' ' ')
|
||||
local removed; removed=$(comm -13 "$extra" "$listed" | tr '\n' ' ')
|
||||
|
||||
[[ -n ${untracked// } ]] && warn "installed but not in packages.txt: $untracked" || skip "nothing installed that packages.txt misses"
|
||||
[[ -n ${removed// } ]] && warn "in packages.txt but not installed: $removed"
|
||||
if [[ -n ${untracked// } ]]; then
|
||||
warn "installed but in no layer's packages.txt: $untracked"
|
||||
say " add each to the layer it belongs to, or to $PROFILE/packages.txt [ignore]"
|
||||
else
|
||||
skip "nothing installed that the layers miss"
|
||||
fi
|
||||
(( ${#missing[@]} )) && warn "listed but not installed: ${missing[*]}"
|
||||
say " packages.txt keeps a 'why' comment per package, so edit it by hand."
|
||||
rm -f "$listed" "$baseline" "$extra"
|
||||
rm -f "$listed" "$baseline"
|
||||
}
|
||||
|
||||
main() {
|
||||
local want=()
|
||||
for a in "$@"; do
|
||||
local want=() profile="" force=0 a
|
||||
while (( $# )); do
|
||||
a=$1; shift
|
||||
case $a in
|
||||
--list|-l) printf '%s\n' "${STEPS[@]}"; exit 0 ;;
|
||||
-h|--help) sed -n '2,13p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
-*) warn "unknown option: $a"; exit 2 ;;
|
||||
--profile|-p) profile=${1:?--profile needs a name}; shift ;;
|
||||
--profile=*) profile=${a#*=} ;;
|
||||
--force) force=1 ;;
|
||||
--list|-l) printf '%s\n' "${STEPS[@]}"; exit 0 ;;
|
||||
-h|--help) awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "${BASH_SOURCE[0]}"; exit 0 ;;
|
||||
-*) warn "unknown option: $a"; exit 2 ;;
|
||||
*) if [[ " ${STEPS[*]} " == *" $a "* ]]; then want+=("$a"); else warn "unknown step: $a"; exit 2; fi ;;
|
||||
esac
|
||||
done
|
||||
(( ${#want[@]} )) || want=("${STEPS[@]}")
|
||||
|
||||
say "${c_hd}capture${c_off} repo=$REPO host=$(hostname)"
|
||||
for s in "${want[@]}"; do "step_$s"; done
|
||||
local saved="" roles=""
|
||||
if def=$(default_profile); then IFS=$'\t' read -r saved roles <<<"$def"; fi
|
||||
[[ -n $profile ]] || profile=$saved
|
||||
[[ -n $profile ]] || { err "no --profile given and none matches host $(hostname)"; exit 2; }
|
||||
local rp; rp=$(find_layer "$profile") || { err "no such profile: $profile"; exit 2; }
|
||||
[[ $rp == "$saved" ]] || roles=""
|
||||
ROLES=(); [[ -n $roles ]] && IFS=', ' read -ra ROLES <<<"$roles"
|
||||
|
||||
# Before the first layered install, the live files still have the old shape
|
||||
# (e.g. bindings in bindings.lua rather than hypr/layers/), and capturing them
|
||||
# would write machine-specific content into shared layers like base/.
|
||||
if [[ ! -f $STATE_FILE ]] && (( ! force )); then
|
||||
err "no layered install recorded on this machine ($STATE_FILE)."
|
||||
say " Run ./install.sh first, or --force and review the diff of shared layers by hand."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
resolve "$rp" "${ROLES[@]}" || exit 2
|
||||
load_settings
|
||||
|
||||
say "${c_hd}capture${c_off} profile=$PROFILE${ROLES:+ roles=${ROLES[*]}} host=$(hostname)"
|
||||
# Canonical order whatever order they were named in: setup after config, etc.
|
||||
for s in "${STEPS[@]}"; do [[ " ${want[*]} " == *" $s "* ]] && "step_$s"; done
|
||||
|
||||
head_ "Done"
|
||||
say " Review with: git -C $REPO diff"
|
||||
|
||||
Reference in New Issue
Block a user