Last active
September 26, 2026 08:37
-
-
Save dzogrim/4a5eaac370bdae8fca30f690e5d947ad to your computer and use it in GitHub Desktop.
Standalone macOS settings helper for nix-plist-manager
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # shellcheck shell=bash | |
| # | |
| # ============================================================================== | |
| # nix-plist-manager-mac | |
| # ============================================================================== | |
| # | |
| # Description: Standalone macOS settings helper for nix-plist-manager. | |
| # | |
| # Provides a simple operational interface to: | |
| # | |
| # capture - Capture current macOS settings into mac.nix | |
| # compare - Compare current macOS settings against stored mac.nix | |
| # apply - Apply the stored configuration through Home Manager | |
| # status - Check environment and configuration status | |
| # | |
| # Configuration: | |
| # Nix directory : $HOME/.config/nix | |
| # Settings file : $HOME/.config/nix/mac.nix | |
| # | |
| # Safety: | |
| # - macOS-only execution | |
| # - Checks Nix availability | |
| # - Validates required configuration files | |
| # - Creates timestamped backups before replacing mac.nix | |
| # - Cleans temporary resources automatically | |
| # - Uses explicit exit codes | |
| # - Disables colours automatically when stdout is not a TTY | |
| # | |
| # Usage: | |
| # nix-plist-manager-mac capture | |
| # nix-plist-manager-mac compare | |
| # nix-plist-manager-mac apply | |
| # nix-plist-manager-mac status | |
| # nix-plist-manager-mac help | |
| # | |
| # ============================================================================== | |
| set -uo pipefail | |
| readonly VERSION="0.1.1" | |
| readonly NIX_DIR="${HOME}/.config/nix" | |
| readonly MAC_NIX="${NIX_DIR}/mac.nix" | |
| readonly NPM_FLAKE="github:SushyDev/nix-plist-manager#current" | |
| # Temporary workspace used by commands requiring transient files. | |
| # Kept global so the EXIT trap can safely access it with nounset enabled. | |
| TEMP_DIR="" | |
| # ============================================================================== | |
| # Terminal formatting | |
| # ============================================================================== | |
| RESET="" | |
| BOLD="" | |
| DIM="" | |
| RED="" | |
| GREEN="" | |
| YELLOW="" | |
| BLUE="" | |
| CYAN="" | |
| init_terminal() | |
| { | |
| if [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then | |
| RESET="$(tput sgr0 2>/dev/null || true)" | |
| BOLD="$(tput bold 2>/dev/null || true)" | |
| DIM="$(tput dim 2>/dev/null || true)" | |
| RED="$(tput setaf 1 2>/dev/null || true)" | |
| GREEN="$(tput setaf 2 2>/dev/null || true)" | |
| YELLOW="$(tput setaf 3 2>/dev/null || true)" | |
| BLUE="$(tput setaf 4 2>/dev/null || true)" | |
| CYAN="$(tput setaf 6 2>/dev/null || true)" | |
| fi | |
| } | |
| # ============================================================================== | |
| # Output helpers | |
| # ============================================================================== | |
| info() | |
| { | |
| printf '%s::%s %s\n' \ | |
| "${BLUE}${BOLD}" "${RESET}" "$*" | |
| } | |
| success() | |
| { | |
| printf '%s✔%s %s\n' \ | |
| "${GREEN}${BOLD}" "${RESET}" "$*" | |
| } | |
| warning() | |
| { | |
| printf '%s!%s %s\n' \ | |
| "${YELLOW}${BOLD}" "${RESET}" "$*" >&2 | |
| } | |
| error() | |
| { | |
| printf '%s✘%s %s\n' \ | |
| "${RED}${BOLD}" "${RESET}" "$*" >&2 | |
| } | |
| header() | |
| { | |
| printf '\n%s%s%s %sv%s%s\n' \ | |
| "${BOLD}${CYAN}" \ | |
| "nix-plist-manager-mac" \ | |
| "${RESET}" \ | |
| "${DIM}" \ | |
| "${VERSION}" \ | |
| "${RESET}" | |
| printf '%s%s%s\n\n' \ | |
| "${DIM}" \ | |
| "Capture, compare and restore declarative macOS preferences." \ | |
| "${RESET}" | |
| } | |
| # ============================================================================== | |
| # Cleanup | |
| # ============================================================================== | |
| cleanup() | |
| { | |
| # TEMP_DIR is intentionally global so this function remains safe when | |
| # invoked by the EXIT trap after command-local variables are out of scope. | |
| if [[ -n "${TEMP_DIR:-}" && -d "${TEMP_DIR}" ]]; then | |
| rm -rf -- "${TEMP_DIR}" | |
| fi | |
| TEMP_DIR="" | |
| } | |
| # Static trap avoids variable expansion at registration time and therefore | |
| # remains ShellCheck-safe (SC2064). | |
| trap cleanup EXIT | |
| # ============================================================================== | |
| # Environment validation | |
| # ============================================================================== | |
| check_macos() | |
| { | |
| local os | |
| os="$(uname -s 2>/dev/null || true)" | |
| if [[ "${os}" != "Darwin" ]]; then | |
| error "Unsupported operating system: ${os:-unknown}" | |
| printf 'This utility can only run on macOS.\n' >&2 | |
| return 1 | |
| fi | |
| } | |
| check_nix() | |
| { | |
| if ! command -v nix >/dev/null 2>&1; then | |
| error "Nix is not available in PATH." | |
| printf '\n' | |
| printf 'A working Nix installation is required.\n' | |
| printf 'Check your Nix installation and shell environment.\n' | |
| return 1 | |
| fi | |
| } | |
| check_mac_nix() | |
| { | |
| if [[ ! -f "${MAC_NIX}" ]]; then | |
| error "Configuration file not found:" | |
| printf ' %s\n\n' "${MAC_NIX}" | |
| printf 'Create it first with:\n' | |
| printf ' nix-plist-manager-mac capture\n' | |
| return 1 | |
| fi | |
| } | |
| preflight() | |
| { | |
| check_macos || return 1 | |
| check_nix || return 1 | |
| } | |
| # ============================================================================== | |
| # Capture | |
| # ============================================================================== | |
| cmd_capture() | |
| { | |
| local backup | |
| preflight || return 1 | |
| header | |
| info "Capturing current macOS settings" | |
| printf 'Destination: %s\n\n' "${MAC_NIX}" | |
| if ! mkdir -p "${NIX_DIR}"; then | |
| error "Unable to create configuration directory:" | |
| printf ' %s\n' "${NIX_DIR}" >&2 | |
| return 1 | |
| fi | |
| # Preserve the previous known-good configuration before overwriting it. | |
| if [[ -f "${MAC_NIX}" ]]; then | |
| backup="${MAC_NIX}.$(date '+%Y%m%d-%H%M%S').bak" | |
| if ! cp -p "${MAC_NIX}" "${backup}"; then | |
| error "Unable to create backup. Capture aborted." | |
| return 1 | |
| fi | |
| success "Previous configuration backed up" | |
| printf ' %s\n\n' "${backup}" | |
| fi | |
| if nix run "${NPM_FLAKE}" -- \ | |
| "${MAC_NIX}" \ | |
| --scope user | |
| then | |
| printf '\n' | |
| success "Capture completed" | |
| printf ' %s\n' "${MAC_NIX}" | |
| else | |
| printf '\n' | |
| error "Capture failed." | |
| return 1 | |
| fi | |
| } | |
| # ============================================================================== | |
| # Compare | |
| # ============================================================================== | |
| cmd_compare() | |
| { | |
| local current_nix | |
| local diff_file | |
| local rc | |
| preflight || return 1 | |
| check_mac_nix || return 1 | |
| header | |
| info "Comparing stored configuration against current macOS settings" | |
| printf 'Stored configuration:\n' | |
| printf ' %s\n\n' "${MAC_NIX}" | |
| # -------------------------------------------------------------------------- | |
| # Create an isolated temporary workspace. | |
| # | |
| # We deliberately do NOT use nix-plist-manager --against here. | |
| # Instead, we capture the current machine state into a temporary mac.nix | |
| # and perform a standard local diff against the stored configuration. | |
| # | |
| # This makes the comparison deterministic and completely transparent: | |
| # | |
| # stored mac.nix <── diff ──> freshly captured mac.nix | |
| # -------------------------------------------------------------------------- | |
| if ! TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/nix-plist-manager.XXXXXX")"; then | |
| error "Unable to create temporary directory." | |
| return 1 | |
| fi | |
| current_nix="${TEMP_DIR}/mac-current.nix" | |
| diff_file="${TEMP_DIR}/mac.diff" | |
| info "Capturing current macOS settings into temporary workspace" | |
| if ! nix run "${NPM_FLAKE}" -- \ | |
| "${current_nix}" \ | |
| --scope user \ | |
| >/dev/null | |
| then | |
| error "Unable to capture current macOS settings." | |
| return 1 | |
| fi | |
| success "Current settings captured" | |
| printf '\n' | |
| # -------------------------------------------------------------------------- | |
| # Compare both local files. | |
| # | |
| # diff exit codes: | |
| # 0 = files are identical | |
| # 1 = files differ | |
| # >1 = operational error | |
| # -------------------------------------------------------------------------- | |
| diff -u \ | |
| --label "stored: ${MAC_NIX}" \ | |
| --label "current: macOS" \ | |
| "${MAC_NIX}" \ | |
| "${current_nix}" \ | |
| >"${diff_file}" | |
| rc=$? | |
| case "${rc}" in | |
| 0) | |
| success "No differences found" | |
| printf '\n' | |
| printf '%sStored configuration matches the current Mac.%s\n' \ | |
| "${DIM}" "${RESET}" | |
| ;; | |
| 1) | |
| warning "Differences found" | |
| printf '\n' | |
| # Use a colour-aware pager when available, while remaining | |
| # perfectly usable on a minimal macOS installation. | |
| if command -v delta >/dev/null 2>&1; then | |
| delta <"${diff_file}" | |
| elif command -v colordiff >/dev/null 2>&1; then | |
| colordiff <"${diff_file}" | |
| elif command -v less >/dev/null 2>&1 && [[ -t 1 ]]; then | |
| less -R "${diff_file}" | |
| else | |
| cat "${diff_file}" | |
| fi | |
| printf '\n' | |
| warning "Stored configuration differs from the current Mac." | |
| ;; | |
| *) | |
| error "diff failed (exit ${rc})." | |
| return "${rc}" | |
| ;; | |
| esac | |
| # Release temporary files immediately. The EXIT trap remains as a safety | |
| # net for errors, interruptions and early returns. | |
| cleanup | |
| return 0 | |
| } | |
| # ============================================================================== | |
| # Apply | |
| # ============================================================================== | |
| cmd_apply() | |
| { | |
| local rc | |
| preflight || return 1 | |
| check_mac_nix || return 1 | |
| header | |
| info "Applying stored macOS settings" | |
| printf 'Source:\n' | |
| printf ' %s\n\n' "${MAC_NIX}" | |
| if [[ ! -f "${NIX_DIR}/flake.nix" ]]; then | |
| error "flake.nix not found:" | |
| printf ' %s\n' "${NIX_DIR}/flake.nix" | |
| return 1 | |
| fi | |
| if ! command -v home-manager >/dev/null 2>&1; then | |
| error "home-manager is not available in PATH." | |
| printf '\n' | |
| printf 'The apply operation currently requires Home Manager.\n' | |
| return 1 | |
| fi | |
| ( | |
| cd "${NIX_DIR}" || exit 1 | |
| home-manager switch --flake . | |
| ) | |
| rc=$? | |
| if (( rc == 0 )); then | |
| printf '\n' | |
| success "Stored settings applied successfully" | |
| else | |
| printf '\n' | |
| error "Home Manager rebuild failed (exit ${rc})." | |
| return "${rc}" | |
| fi | |
| } | |
| # ============================================================================== | |
| # Status | |
| # ============================================================================== | |
| cmd_status() | |
| { | |
| local os | |
| local arch | |
| header | |
| os="$(uname -s 2>/dev/null || printf 'unknown')" | |
| arch="$(uname -m 2>/dev/null || printf 'unknown')" | |
| printf '%-20s %s (%s)\n' \ | |
| "Platform:" "${os}" "${arch}" | |
| if command -v nix >/dev/null 2>&1; then | |
| printf '%-20s %s%s%s\n' \ | |
| "Nix:" "${GREEN}" "available" "${RESET}" | |
| printf '%-20s %s\n' \ | |
| "Nix version:" "$(nix --version 2>/dev/null || printf 'unknown')" | |
| else | |
| printf '%-20s %s%s%s\n' \ | |
| "Nix:" "${RED}" "not found" "${RESET}" | |
| fi | |
| if command -v home-manager >/dev/null 2>&1; then | |
| printf '%-20s %s%s%s\n' \ | |
| "Home Manager:" "${GREEN}" "available" "${RESET}" | |
| else | |
| printf '%-20s %s%s%s\n' \ | |
| "Home Manager:" "${YELLOW}" "not found" "${RESET}" | |
| fi | |
| if [[ -f "${MAC_NIX}" ]]; then | |
| printf '%-20s %s%s%s\n' \ | |
| "mac.nix:" "${GREEN}" "present" "${RESET}" | |
| printf '%-20s %s\n' \ | |
| "Last modified:" \ | |
| "$(stat -f '%Sm' -t '%Y-%m-%d %H:%M:%S' "${MAC_NIX}" 2>/dev/null || printf 'unknown')" | |
| else | |
| printf '%-20s %s%s%s\n' \ | |
| "mac.nix:" "${YELLOW}" "missing" "${RESET}" | |
| fi | |
| printf '\n%-20s %s\n' \ | |
| "Configuration:" "${MAC_NIX}" | |
| } | |
| # ============================================================================== | |
| # Help | |
| # ============================================================================== | |
| usage() | |
| { | |
| header | |
| cat <<EOF | |
| ${BOLD}Usage:${RESET} | |
| nix-plist-manager-mac <command> | |
| ${BOLD}Commands:${RESET} | |
| ${GREEN}capture${RESET} Capture current macOS settings into mac.nix | |
| ${CYAN}compare${RESET} Compare current macOS settings against mac.nix | |
| ${YELLOW}apply${RESET} Apply mac.nix through Home Manager | |
| status Show environment and configuration status | |
| version Show program version | |
| help Show this help | |
| ${BOLD}Aliases:${RESET} | |
| diff → compare | |
| restore → apply | |
| check → status | |
| ${BOLD}Data flow:${RESET} | |
| capture macOS ─────────────────────────► mac.nix | |
| compare macOS ─► temporary snapshot ─┐ | |
| ├─► local diff | |
| mac.nix ─────────────────────┘ | |
| apply macOS ◄──────────────────────── mac.nix | |
| ${BOLD}Configuration:${RESET} | |
| ${MAC_NIX} | |
| EOF | |
| } | |
| # ============================================================================== | |
| # Main | |
| # ============================================================================== | |
| main() | |
| { | |
| init_terminal | |
| case "${1:-help}" in | |
| capture) | |
| cmd_capture | |
| ;; | |
| compare|diff) | |
| cmd_compare | |
| ;; | |
| apply|restore) | |
| cmd_apply | |
| ;; | |
| status|check) | |
| cmd_status | |
| ;; | |
| version|-V|--version) | |
| printf 'nix-plist-manager-mac %s\n' "${VERSION}" | |
| ;; | |
| help|-h|--help) | |
| usage | |
| ;; | |
| *) | |
| error "Unknown command: ${1}" | |
| printf '\n' | |
| usage | |
| return 2 | |
| ;; | |
| esac | |
| } | |
| # ============================================================================== | |
| # Standalone entry point | |
| # ============================================================================== | |
| main "$@" | |
| exit $? |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment