Files
omarchy-setup/lib/layers.sh
T
noconnorandClaude Opus 5.5 87a31858fd 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
2026-10-16 16:01:54 -04:00

184 lines
6.5 KiB
Bash

# shellcheck shell=bash disable=SC1090
# Profile resolution, shared by install.sh and capture.sh. Sourced, not run.
#
# A profile is a directory under profiles/ with a `profile` file (bash, sourced)
# and any of: config/ (mirrors ~/.config), themes/, plugins.txt,
# plugin-patches/, bin/, systemd/user/, system/ (mirrors /etc), handoff/,
# packages.txt, setup.sh. `include=(...)` names the layers it builds on.
#
# Resolving a profile gives an ordered list of layers: each layer's includes
# first (depth first, each layer once), then any --roles, then the profile
# itself. For a path that several layers provide, the LAST layer wins, so a
# machine always overrides the roles and shared layers under it.
#
# Expects REPO to be set, and err/warn helpers from the caller.
PROFILES=$REPO/profiles
STATE_FILE=$HOME/.config/omarchy-setup/profile
LAYERS=() # resolved layer paths relative to profiles/, in order
PROFILE="" # the profile asked for, e.g. machines/hp-laptop
ROLES=() # extra roles asked for, e.g. devops ai-agents
_resolving=()
# find_layer <name> -> layer path relative to profiles/. Accepts the full path
# (machines/hp-laptop) or a bare name looked up in machines/, roles/, features/.
find_layer() {
local c
for c in "$1" "machines/$1" "roles/$1" "features/$1"; do
[[ -f $PROFILES/$c/profile ]] && { printf '%s\n' "$c"; return 0; }
done
return 1
}
layer_includes() {
( include=(); source "$PROFILES/$1/profile"; printf '%s\n' "${include[@]}" )
}
layer_description() {
( description=""; source "$PROFILES/$1/profile"; printf '%s\n' "$description" )
}
_add_layer() {
local rel l inc
rel=$(find_layer "$1") || { err "no such layer: $1"; return 1; }
for l in "${LAYERS[@]}"; do [[ $l == "$rel" ]] && return 0; done
[[ " ${_resolving[*]} " == *" $rel "* ]] && { err "include cycle through $rel"; return 1; }
_resolving+=("$rel")
while IFS= read -r inc; do
[[ -n $inc ]] || continue
_add_layer "$inc" || return 1
done < <(layer_includes "$rel")
LAYERS+=("$rel")
}
# resolve <profile> [role...] -> fills LAYERS
resolve() {
local rel inc r
LAYERS=(); _resolving=()
rel=$(find_layer "$1") || { err "no such profile: $1 (./install.sh --profiles lists them)"; return 1; }
PROFILE=$rel
_resolving+=("$rel")
while IFS= read -r inc; do
[[ -n $inc ]] || continue
_add_layer "$inc" || return 1
done < <(layer_includes "$rel")
for r in "${@:2}"; do _add_layer "roles/${r#roles/}" || return 1; done
LAYERS+=("$rel")
}
# Settings from the layers' profile files. Arrays accumulate across layers;
# handoff_peer_ips and arch come from the last layer that sets them.
P_ARCH=""; P_HOSTNAMES=(); P_HANDOFF_IPS=(); P_DOTFILES=(); P_AETHER=()
load_settings() {
local l
P_ARCH=""; P_HOSTNAMES=(); P_HANDOFF_IPS=(); P_DOTFILES=(); P_AETHER=()
for l in "${LAYERS[@]}"; do
local arch="" hostnames=() handoff_peer_ips=() dotfiles=() aether_themes=() include=() description=""
source "$PROFILES/$l/profile"
[[ -n $arch ]] && P_ARCH=$arch
(( ${#hostnames[@]} )) && P_HOSTNAMES=("${hostnames[@]}")
(( ${#handoff_peer_ips[@]} )) && P_HANDOFF_IPS=("${handoff_peer_ips[@]}")
P_DOTFILES+=("${dotfiles[@]}")
P_AETHER+=("${aether_themes[@]}")
done
}
# Which profile to use when none is given: the one recorded by the last
# install on this machine, else the machine profile whose hostnames= matches.
# Prints "profile<TAB>role,role".
default_profile() {
if [[ -f $STATE_FILE ]]; then
local profile="" roles=""
source "$STATE_FILE"
[[ -n $profile ]] && { printf '%s\t%s\n' "$profile" "$roles"; return 0; }
fi
local f host; host=$(hostname 2>/dev/null || cat /etc/hostname)
for f in "$PROFILES"/machines/*/profile; do
if ( hostnames=(); source "$f"; [[ " ${hostnames[*]} " == *" $host "* ]] ); then
f=${f%/profile}; printf '%s\t\n' "${f#"$PROFILES/"}"; return 0
fi
done
return 1
}
save_state() {
mkdir -p "${STATE_FILE%/*}"
local IFS=,
printf '# Written by omarchy-setup/install.sh; read when no --profile is given.\nprofile=%q\nroles=%q\n' \
"$PROFILE" "${ROLES[*]}" > "$STATE_FILE"
}
# owners <subdir> -> "relpath<TAB>layer" for every file under <layer>/<subdir>
# across the layers, the last layer winning each relpath. Sorted by relpath.
owners() {
local sub=$1 l f
declare -A own=()
for l in "${LAYERS[@]}"; do
[[ -d $PROFILES/$l/$sub ]] || continue
while IFS= read -r -d '' f; do
own[${f#"$PROFILES/$l/$sub/"}]=$l
done < <(find "$PROFILES/$l/$sub" -type f -print0)
done
for f in "${!own[@]}"; do printf '%s\t%s\n' "$f" "${own[$f]}"; done | sort
}
# Every layer that has <path> (a file or directory), in order.
layers_with() {
local l
for l in "${LAYERS[@]}"; do [[ -e $PROFILES/$l/$1 ]] && printf '%s\n' "$l"; done
}
# Parse every layer's packages.txt into PKG_<section> arrays of
# "name<US>archs<US>cmds<US>layer", US being $PKG_FS. Not a tab: tab is IFS
# whitespace, so `read` would collapse an empty archs field and shift the rest.
# [ignore] lines may hold several names.
PKG_FS=$'\x1f'
PKG_repo=(); PKG_aur=(); PKG_optional=(); PKG_ignore=()
load_packages() {
local l line section tok name archs cmds
PKG_repo=(); PKG_aur=(); PKG_optional=(); PKG_ignore=()
declare -A seen=()
for l in "${LAYERS[@]}"; do
[[ -f $PROFILES/$l/packages.txt ]] || continue
section=""
while IFS= read -r line; do
line=${line%%#*}
read -ra tok <<<"$line"
(( ${#tok[@]} )) || continue
case ${tok[0]} in
'[repo]'|'[aur]'|'[optional]'|'[ignore]') section=${tok[0]//[\[\]]/}; continue ;;
esac
[[ -n $section ]] || continue
if [[ $section == ignore ]]; then
for name in "${tok[@]}"; do PKG_ignore+=("$name$PKG_FS$PKG_FS$PKG_FS$l"); done
continue
fi
name=${tok[0]}; archs=""; cmds=""
for t in "${tok[@]:1}"; do
case $t in
cmd=*) cmds=${t#cmd=} ;;
x86_64|aarch64) archs+="$t," ;;
*) warn "$l/packages.txt: unknown token '$t' on $name" ;;
esac
done
[[ -n ${seen[$name]:-} ]] && continue
seen[$name]=1
local -n arr="PKG_$section"
arr+=("$name$PKG_FS${archs%,}$PKG_FS$cmds$PKG_FS$l")
unset -n arr
done < "$PROFILES/$l/packages.txt"
done
}
# pkg_applies <archs> -> true when this machine's arch is in the list (or no list)
pkg_applies() { [[ -z $1 || ",$1," == *",$(uname -m),"* ]]; }
# pkg_cmd_present <cmds> -> true when any of the comma-separated commands exists
pkg_cmd_present() {
local c
[[ -n $1 ]] || return 1
for c in ${1//,/ }; do command -v "$c" >/dev/null 2>&1 && return 0; done
return 1
}