tmux 3.1 added an XDG location and it takes PRECEDENCE over ~/.tmux.conf. Verified on 3.4 here by writing a different marker into each and asking tmux which one it ended up with: both present -> ~/.config/tmux/tmux.conf only ~/.tmux.conf -> ~/.tmux.conf only the XDG one -> the XDG one So the Shell section writing ~/.tmux.conf on a machine that already has the XDG file produced a file tmux will never read, and reported "tmux config installed" having changed nothing anybody could observe. That is the worst shape a config step can have: it looks done. tmux_config_target now picks the path tmux will actually load — the existing XDG file if there is one, otherwise ~/.tmux.conf, which is still what every guide names and what a machine with neither should get. When both exist the section says so out loud before targeting the winner, because "your other file wins" is not something anyone infers from a success message. Also removed an untracked duplicate at scripts/setup/.tmux.conf. The one the script installs is scripts/setup/machine-setup/.tmux.conf — SCRIPT_DIR is the machine-setup directory — and two identical copies with only one of them read is the drift this whole evening has been about. Nothing to change about the config itself: the tracked copy is already byte-for- byte the owner's own ~/.tmux.conf. Not yet wired into per-user provisioning. src/servers/shell-skel/ seeds a zshrc for a member and has no tmux config beside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
163 lines
5.8 KiB
Bash
163 lines
5.8 KiB
Bash
#!/bin/bash
|
|
# =============================================================================
|
|
# machine-setup — writing files into somebody's home
|
|
# =============================================================================
|
|
#
|
|
# Definitions only, like the other lib/ files.
|
|
#
|
|
# ── The rule ──
|
|
#
|
|
# A setup script may create a config file. It may not silently replace one the
|
|
# user wrote. The original did the second: `cp .tmux.conf $USER_HOME/` on every
|
|
# run, over whatever was there, and five separate `cat >>` into .zshrc with no
|
|
# guard — so a second pass duplicated the starship init, the nvim PATH, bun, deno
|
|
# and the aliases.
|
|
#
|
|
# Both of those are the same mistake in different shapes: writing without looking
|
|
# first. The two helpers here are the two safe shapes.
|
|
|
|
[[ -n "${MACHINE_SETUP_FILES_LOADED:-}" ]] && return 0
|
|
MACHINE_SETUP_FILES_LOADED=1
|
|
|
|
# Put a config file in place, asking before it replaces one the user has.
|
|
#
|
|
# Three outcomes, and the caller can tell them apart by the return code:
|
|
#
|
|
# 0 installed — there was nothing there, or the user chose to replace
|
|
# 1 identical — already exactly this, nothing done
|
|
# 2 kept — the user chose to keep theirs
|
|
#
|
|
# When the file exists and differs, this ASKS rather than deciding. Silently
|
|
# keeping theirs is safe but unhelpful — they never learn that a newer version
|
|
# exists — and silently replacing it is how a setup script eats somebody's
|
|
#configuration. So: keep, replace, or show the difference first, and a replaced file is
|
|
# always kept beside the new one.
|
|
#
|
|
# NOTE for callers: 1 and 2 are outcomes, not failures — but they are still
|
|
# non-zero, so calling this as a plain command under `set -e` ends the script
|
|
# before the result can be read. Always capture it:
|
|
#
|
|
# install_config "$src" "$dest" "$user" && rc=0 || rc=$?
|
|
install_config() {
|
|
local src="$1" dest="$2" owner="$3" answer
|
|
|
|
if [[ ! -f "$dest" ]]; then
|
|
install -D -m 0644 -o "$owner" -g "$(user_group "$owner")" "$src" "$dest"
|
|
return 0
|
|
fi
|
|
|
|
cmp -s "$src" "$dest" && return 1
|
|
|
|
echo ""
|
|
warn "${dest} already exists here, and differs from the one this script ships."
|
|
|
|
# Never replace a file the user has without being told to. An unattended run
|
|
# answers "keep", because the alternative is destroying configuration nobody
|
|
# was present to defend.
|
|
if [[ "${ASSUME_YES:-}" == "1" ]] || [[ ! -t 0 ]]; then
|
|
echo " keeping yours (nothing was asked, so nothing is replaced)"
|
|
return 2
|
|
fi
|
|
|
|
while true; do
|
|
echo " [1] keep yours — nothing changes"
|
|
echo " [2] use ours — yours is kept as ${dest}.before-machine-setup"
|
|
echo " [3] show me the difference first"
|
|
echo ""
|
|
if ! read -rp " Which one? (1/2/3) [1]: " answer; then
|
|
echo ""
|
|
echo " keeping yours"
|
|
return 2
|
|
fi
|
|
case "${answer:-1}" in
|
|
1)
|
|
echo " keeping yours"
|
|
return 2
|
|
;;
|
|
2)
|
|
cp -a "$dest" "${dest}.before-machine-setup"
|
|
install -D -m 0644 -o "$owner" -g "$(user_group "$owner")" "$src" "$dest"
|
|
ok "replaced — yours is at ${dest}.before-machine-setup"
|
|
return 0
|
|
;;
|
|
3)
|
|
echo ""
|
|
# yours on the left, ours on the right: - is what you would lose,
|
|
# + is what you would gain.
|
|
diff -u --label "yours: ${dest}" --label "ours: ${src}" "$dest" "$src" | page
|
|
echo ""
|
|
;;
|
|
*) warn "Pick 1, 2 or 3." ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Append a block to a file exactly once.
|
|
#
|
|
# The block is wrapped in markers naming what it is, so a second run recognises
|
|
# its own work instead of adding it again — and so a human reading the file can
|
|
# see which lines came from here and delete them as a unit.
|
|
#
|
|
# append_once ~/.zshrc bun <<'EOF'
|
|
# export PATH="$HOME/.bun/bin:$PATH"
|
|
# EOF
|
|
#
|
|
# Returns 0 if it wrote, 1 if the block was already there.
|
|
#
|
|
# One limitation, and it bites the author rather than the user: RENAMING a marker
|
|
# orphans the block that used the old name. append_once only recognises the name
|
|
# it is given, so the previous block stays in the file doing whatever it did.
|
|
# Changing a block's CONTENT has the same shape — the marker is found, so the new
|
|
# content is never written. Both need the old block removed by hand.
|
|
append_once() {
|
|
local file="$1" name="$2"
|
|
local begin="# >>> machine-setup: ${name} >>>"
|
|
local end="# <<< machine-setup: ${name} <<<"
|
|
|
|
if [[ -f "$file" ]] && grep -qF "$begin" "$file"; then
|
|
return 1
|
|
fi
|
|
|
|
{
|
|
echo ""
|
|
echo "$begin"
|
|
cat
|
|
echo "$end"
|
|
} >>"$file"
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Where tmux actually reads its config
|
|
# -----------------------------------------------------------------------------
|
|
#
|
|
# tmux 3.1 added an XDG location and it takes PRECEDENCE. Verified on 3.4 by
|
|
# creating both and asking tmux which marker it ended up with:
|
|
#
|
|
# both present -> ~/.config/tmux/tmux.conf
|
|
# only ~/.tmux.conf -> ~/.tmux.conf
|
|
# only the XDG one -> the XDG one
|
|
#
|
|
# So installing to ~/.tmux.conf on a machine that has the XDG file writes a file
|
|
# tmux will never read, and the script would report success having changed
|
|
# nothing anybody can see. That is the failure this exists to prevent.
|
|
#
|
|
# Rules, in order:
|
|
# 1. an existing XDG config wins -> that is their real config, target it
|
|
# 2. an existing ~/.tmux.conf -> target it, since it is what tmux reads
|
|
# 3. neither -> ~/.tmux.conf, the path every guide names
|
|
tmux_config_target() {
|
|
local home="$1"
|
|
local xdg="${XDG_CONFIG_HOME:-$home/.config}/tmux/tmux.conf"
|
|
if [[ -f "$xdg" ]]; then
|
|
echo "$xdg"
|
|
else
|
|
echo "$home/.tmux.conf"
|
|
fi
|
|
}
|
|
|
|
# True when a ~/.tmux.conf would be shadowed by an XDG config that already exists.
|
|
tmux_dot_conf_is_shadowed() {
|
|
local home="$1"
|
|
[[ -f "${XDG_CONFIG_HOME:-$home/.config}/tmux/tmux.conf" && -f "$home/.tmux.conf" ]]
|
|
}
|