Snapshot and restore macOS preference domains
Adds a round-trip mechanism for macOS system preferences that are not
covered by Homebrew or Mackup (Dock layout, Finder options, global
keyboard shortcuts, input sources, Spaces config, menu-bar clock,
NSGlobalDomain).
sync: `ssr sync` exports each configured domain with `defaults export`
and writes one plist per domain to <backup_dir>/macos-prefs/. Export is
soft-fail per domain so one bad domain doesn't abort the sync. Gated by
SSR_SNAPSHOT_MACOS_PREFS=true (default on).
restore: `ssr restore` replays the snapshot with `defaults import` after
the declarative config/macos-defaults.sh runs, so the snapshot is the
last writer and wins for accepted domains. Before importing, the restore
detects domains that both the snapshot and macos-defaults.sh touch and
prompts interactively:
Domain `com.apple.dock`:
Snapshot will OVERRIDE values set by macos-defaults.sh.
[A]ccept snapshot / [r]eject (keep declarative)? [A/r]:
Non-TTY (launchd, CI) falls back to SSR_PREFS_CONFLICT_DEFAULT (default
"accept"). SSR_ASSUME_YES=true skips all prompts. After import, Dock,
Finder, SystemUIServer and cfprefsd are restarted once.
New config keys: SSR_SNAPSHOT_MACOS_PREFS, SSR_MACOS_PREF_DOMAINS,
SSR_MACOS_PREF_DOMAINS_EXTRA, SSR_PREFS_CONFLICT_DEFAULT, SSR_ASSUME_YES.
ssr-status surfaces the snapshotted-domain count.
README documents the snapshot flow, default domain table, conflict UX,
and the five new config keys.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -71,7 +71,11 @@ Output structure in the cloud drive:
|
|||||||
│ └── <hostname> - <hardware-uuid>/
|
│ └── <hostname> - <hardware-uuid>/
|
||||||
│ ├── Brewfile
|
│ ├── Brewfile
|
||||||
│ ├── Brewfile.bak
|
│ ├── Brewfile.bak
|
||||||
│ └── unmanaged-apps.md # apps installed manually (not brew/MAS)
|
│ ├── unmanaged-apps.md # apps installed manually (not brew/MAS)
|
||||||
|
│ └── macos-prefs/ # pref-domain snapshots
|
||||||
|
│ ├── com.apple.dock.plist
|
||||||
|
│ ├── com.apple.finder.plist
|
||||||
|
│ └── …
|
||||||
└── Mackup/
|
└── Mackup/
|
||||||
└── … (managed by mackup)
|
└── … (managed by mackup)
|
||||||
```
|
```
|
||||||
@@ -89,6 +93,46 @@ Link selection for each app uses, in order:
|
|||||||
2. **Known vendor map** — bundle-id prefix maps to the official download page (Adobe, Microsoft, Docker, Cursor, Anthropic, JetBrains, GitKraken, Signal, Zoom, etc.).
|
2. **Known vendor map** — bundle-id prefix maps to the official download page (Adobe, Microsoft, Docker, Cursor, Anthropic, JetBrains, GitKraken, Signal, Zoom, etc.).
|
||||||
3. **Web search** — DuckDuckGo query for `<App name> macOS download`.
|
3. **Web search** — DuckDuckGo query for `<App name> macOS download`.
|
||||||
|
|
||||||
|
### macOS preferences snapshot
|
||||||
|
|
||||||
|
`ssr sync` also snapshots the live state of a curated set of macOS preference domains, writing one plist per domain to `<backup_dir>/macos-prefs/`. During `ssr restore` these are replayed **after** `config/macos-defaults.sh`, so the snapshot is the last writer.
|
||||||
|
|
||||||
|
Default domains:
|
||||||
|
|
||||||
|
| Domain | What it covers |
|
||||||
|
|---|---|
|
||||||
|
| `com.apple.dock` | Dock position, app layout, hot corners |
|
||||||
|
| `com.apple.finder` | View options, sidebar, path bar |
|
||||||
|
| `com.apple.symbolichotkeys` | Global keyboard shortcuts (Spotlight, Mission Control, Screenshots…) |
|
||||||
|
| `com.apple.HIToolbox` | Input sources / keyboard layouts |
|
||||||
|
| `com.apple.spaces` | Spaces / Mission Control layout |
|
||||||
|
| `com.apple.menuextra.clock` | Menu-bar clock format |
|
||||||
|
| `NSGlobalDomain` | Global UI prefs, key-repeat speed, sound, language |
|
||||||
|
|
||||||
|
**Conflict resolution during restore**
|
||||||
|
|
||||||
|
When a snapshot domain overlaps with a domain that `config/macos-defaults.sh` also writes to, `ssr restore` warns and asks interactively:
|
||||||
|
|
||||||
|
```
|
||||||
|
[WARN] Pref-snapshot overlaps with declarative defaults for 2 domain(s):
|
||||||
|
com.apple.dock, com.apple.finder
|
||||||
|
|
||||||
|
Domain `com.apple.dock`:
|
||||||
|
Snapshot from last sync (2026-05-11T17:30:00+0200) will OVERRIDE values
|
||||||
|
set by macos-defaults.sh.
|
||||||
|
[A]ccept snapshot / [r]eject (keep declarative)? [A/r]:
|
||||||
|
```
|
||||||
|
|
||||||
|
Default = `Accept` (press Enter). Reply `r` to keep the declarative value for that domain. In non-interactive contexts (launchd, CI) the decision falls back to `SSR_PREFS_CONFLICT_DEFAULT` (default `accept`). Set `SSR_ASSUME_YES=true` to skip all prompts.
|
||||||
|
|
||||||
|
Extend the domain set in `~/.config/ssr/ssr.conf`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SSR_MACOS_PREF_DOMAINS_EXTRA="com.apple.universalaccess com.apple.Spotlight"
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Mackup overlap:** if you also configure mackup to sync `com.apple.dock` or similar domains, two mechanisms will compete. Pick one.
|
||||||
|
|
||||||
### Restore on a new machine
|
### Restore on a new machine
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -119,6 +163,11 @@ Config lives in `~/.config/ssr/ssr.conf` (override with `--config` or `$SSR_CONF
|
|||||||
| `SSR_BREW_CLEANUP` | `true` | Run `brew cleanup` after sync |
|
| `SSR_BREW_CLEANUP` | `true` | Run `brew cleanup` after sync |
|
||||||
| `SSR_SCAN_UNMANAGED_APPS` | `true` | List `.app` bundles not from brew/MAS during sync |
|
| `SSR_SCAN_UNMANAGED_APPS` | `true` | List `.app` bundles not from brew/MAS during sync |
|
||||||
| `SSR_APP_DIRS` | `/Applications:~/Applications` | Colon-separated dirs scanned for unmanaged apps |
|
| `SSR_APP_DIRS` | `/Applications:~/Applications` | Colon-separated dirs scanned for unmanaged apps |
|
||||||
|
| `SSR_SNAPSHOT_MACOS_PREFS` | `true` | Snapshot macOS pref domains during sync |
|
||||||
|
| `SSR_MACOS_PREF_DOMAINS` | _(see below)_ | Full override of snapshotted domains (space-separated) |
|
||||||
|
| `SSR_MACOS_PREF_DOMAINS_EXTRA` | — | Extra domains appended to the default set |
|
||||||
|
| `SSR_PREFS_CONFLICT_DEFAULT` | `accept` | Non-TTY conflict resolution: `accept` or `reject` |
|
||||||
|
| `SSR_ASSUME_YES` | `false` | Auto-accept all pref-snapshot conflicts on restore |
|
||||||
| `SSR_DISABLE_GATEKEEPER` | `false` | Run `spctl --global-disable` during restore (security trade-off) |
|
| `SSR_DISABLE_GATEKEEPER` | `false` | Run `spctl --global-disable` during restore (security trade-off) |
|
||||||
| `SSR_OPEN_CASKS` | `false` | Open every installed cask after restore for first-run config |
|
| `SSR_OPEN_CASKS` | `false` | Open every installed cask after restore for first-run config |
|
||||||
| `SSR_MACOS_DEFAULTS` | repo's `config/macos-defaults.sh` | Override path to your own defaults script |
|
| `SSR_MACOS_DEFAULTS` | repo's `config/macos-defaults.sh` | Override path to your own defaults script |
|
||||||
@@ -234,7 +283,8 @@ ssr update --dry-run # show what would change, don't pull
|
|||||||
│ ├── brew.sh # Homebrew install + Brewfile ops
|
│ ├── brew.sh # Homebrew install + Brewfile ops
|
||||||
│ ├── mackup.sh # Mackup install + backup/restore
|
│ ├── mackup.sh # Mackup install + backup/restore
|
||||||
│ ├── apps.sh # Detect unmanaged .app bundles (not brew/MAS)
|
│ ├── apps.sh # Detect unmanaged .app bundles (not brew/MAS)
|
||||||
│ └── macos.sh # macOS-specific actions (Rosetta, defaults, etc.)
|
│ ├── macos.sh # macOS-specific actions (Rosetta, defaults, etc.)
|
||||||
|
│ └── macos-prefs.sh # Snapshot/restore macOS pref domains
|
||||||
├── config/
|
├── config/
|
||||||
│ ├── ssr.conf.example # Reference user config
|
│ ├── ssr.conf.example # Reference user config
|
||||||
│ └── macos-defaults.sh # Default macOS preferences
|
│ └── macos-defaults.sh # Default macOS preferences
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ source "$SSR_ROOT/lib/brew.sh"
|
|||||||
source "$SSR_ROOT/lib/mackup.sh"
|
source "$SSR_ROOT/lib/mackup.sh"
|
||||||
# shellcheck source=../lib/macos.sh
|
# shellcheck source=../lib/macos.sh
|
||||||
source "$SSR_ROOT/lib/macos.sh"
|
source "$SSR_ROOT/lib/macos.sh"
|
||||||
|
# shellcheck source=../lib/macos-prefs.sh
|
||||||
|
source "$SSR_ROOT/lib/macos-prefs.sh"
|
||||||
|
|
||||||
ssr::require_macos
|
ssr::require_macos
|
||||||
ssr::load_config
|
ssr::load_config
|
||||||
@@ -54,4 +56,10 @@ ssr::mackup::restore
|
|||||||
ssr::macos::open_installed_casks
|
ssr::macos::open_installed_casks
|
||||||
ssr::macos::apply_defaults
|
ssr::macos::apply_defaults
|
||||||
|
|
||||||
|
# Import the macOS pref snapshot AFTER the declarative defaults so the
|
||||||
|
# snapshot is the last writer (wins for accepted domains).
|
||||||
|
if [[ "${SSR_SNAPSHOT_MACOS_PREFS:-true}" == "true" ]]; then
|
||||||
|
ssr::macos::prefs_import "$backup_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
ssr::ok "ssr restore completed"
|
ssr::ok "ssr restore completed"
|
||||||
|
|||||||
+10
-5
@@ -30,11 +30,16 @@ else
|
|||||||
echo " last sync: never"
|
echo " last sync: never"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Surface unmanaged-app count from the last sync, if a report exists.
|
# Surface unmanaged-app count and pref-snapshot summary from the last sync.
|
||||||
if backup_dir="$(ssr::cloud::backup_dir 2>/dev/null)" \
|
if backup_dir="$(ssr::cloud::backup_dir 2>/dev/null)"; then
|
||||||
&& [[ -f "$backup_dir/unmanaged-apps.md" ]]; then
|
if [[ -f "$backup_dir/unmanaged-apps.md" ]]; then
|
||||||
n="$(grep -cE '^\| \[' "$backup_dir/unmanaged-apps.md" 2>/dev/null || echo 0)"
|
n="$(grep -cE '^\| \[' "$backup_dir/unmanaged-apps.md" 2>/dev/null || echo 0)"
|
||||||
echo " unmanaged: $n app(s) (see $backup_dir/unmanaged-apps.md)"
|
echo " unmanaged: $n app(s) (see $backup_dir/unmanaged-apps.md)"
|
||||||
|
fi
|
||||||
|
if [[ -d "$backup_dir/macos-prefs" ]]; then
|
||||||
|
n="$(find "$backup_dir/macos-prefs" -name '*.plist' 2>/dev/null | wc -l | tr -d ' ')"
|
||||||
|
echo " prefs snap: $n domain(s) (see $backup_dir/macos-prefs/)"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
label="${SSR_LAUNCHD_LABEL:-de.surke.ssr.sync}"
|
label="${SSR_LAUNCHD_LABEL:-de.surke.ssr.sync}"
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ source "$SSR_ROOT/lib/brew.sh"
|
|||||||
source "$SSR_ROOT/lib/mackup.sh"
|
source "$SSR_ROOT/lib/mackup.sh"
|
||||||
# shellcheck source=../lib/apps.sh
|
# shellcheck source=../lib/apps.sh
|
||||||
source "$SSR_ROOT/lib/apps.sh"
|
source "$SSR_ROOT/lib/apps.sh"
|
||||||
|
# shellcheck source=../lib/macos-prefs.sh
|
||||||
|
source "$SSR_ROOT/lib/macos-prefs.sh"
|
||||||
|
|
||||||
ssr::require_macos
|
ssr::require_macos
|
||||||
ssr::load_config
|
ssr::load_config
|
||||||
@@ -54,6 +56,12 @@ if [[ "${SSR_SCAN_UNMANAGED_APPS:-true}" == "true" ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Snapshot macOS preference domains so they can be restored on a new machine.
|
||||||
|
if [[ "${SSR_SNAPSHOT_MACOS_PREFS:-true}" == "true" ]]; then
|
||||||
|
ssr::log "Snapshotting macOS preferences"
|
||||||
|
ssr::macos::prefs_export "$backup_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
# Record sync time so `ssr status` can report freshness.
|
# Record sync time so `ssr status` can report freshness.
|
||||||
state_dir="${SSR_STATE_DIR:-$HOME/.local/state/ssr}"
|
state_dir="${SSR_STATE_DIR:-$HOME/.local/state/ssr}"
|
||||||
ssr::ensure_dir "$state_dir"
|
ssr::ensure_dir "$state_dir"
|
||||||
|
|||||||
@@ -32,6 +32,27 @@ SSR_SCAN_UNMANAGED_APPS=true
|
|||||||
# Colon-separated directories to scan (top level only).
|
# Colon-separated directories to scan (top level only).
|
||||||
# SSR_APP_DIRS=/Applications:$HOME/Applications
|
# SSR_APP_DIRS=/Applications:$HOME/Applications
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# macOS preferences snapshot (sync + restore)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Snapshot configured pref domains to <backup_dir>/macos-prefs/ during sync
|
||||||
|
# and replay them at the end of restore (after macos-defaults.sh).
|
||||||
|
SSR_SNAPSHOT_MACOS_PREFS=true
|
||||||
|
|
||||||
|
# Full override of the domain list (space-separated). Uncomment to replace
|
||||||
|
# the built-in default set.
|
||||||
|
# SSR_MACOS_PREF_DOMAINS="com.apple.dock com.apple.finder com.apple.symbolichotkeys com.apple.HIToolbox com.apple.spaces com.apple.menuextra.clock NSGlobalDomain"
|
||||||
|
|
||||||
|
# Append extra domains to the default set without replacing it.
|
||||||
|
# SSR_MACOS_PREF_DOMAINS_EXTRA="com.apple.universalaccess com.apple.Spotlight"
|
||||||
|
|
||||||
|
# How to resolve conflicts when restore is non-interactive (no TTY, launchd).
|
||||||
|
# "accept" = snapshot wins; "reject" = declarative macos-defaults.sh wins.
|
||||||
|
SSR_PREFS_CONFLICT_DEFAULT=accept
|
||||||
|
|
||||||
|
# Set to "true" to skip all conflict prompts and auto-accept everywhere.
|
||||||
|
# SSR_ASSUME_YES=false
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# macOS restore behavior
|
# macOS restore behavior
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# lib/macos-prefs.sh — snapshot and restore macOS preference domains.
|
||||||
|
#
|
||||||
|
# Sync writes one plist per domain to <backup_dir>/macos-prefs/.
|
||||||
|
# Restore replays those plists with `defaults import`, but first checks for
|
||||||
|
# overlap with the declarative config/macos-defaults.sh and prompts the user
|
||||||
|
# to Accept (snapshot wins) or Reject (declarative wins) per domain.
|
||||||
|
#
|
||||||
|
# Uses the `defaults` daemon API (never raw file copies) so cfprefsd is
|
||||||
|
# always the authoritative source and caching issues are avoided.
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Default domain list (user can override or append via ssr.conf)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SSR_MACOS_PREF_DOMAINS_DEFAULT=(
|
||||||
|
com.apple.dock
|
||||||
|
com.apple.finder
|
||||||
|
com.apple.symbolichotkeys # global keyboard shortcuts (Mission Control, Spotlight…)
|
||||||
|
com.apple.HIToolbox # input sources / keyboard layout
|
||||||
|
com.apple.spaces # Spaces / Mission Control layout
|
||||||
|
com.apple.menuextra.clock # menu-bar clock format
|
||||||
|
NSGlobalDomain # a.k.a. .GlobalPreferences (UI/keyboard-repeat/sound…)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build the effective domain list: defaults + optional extra from config.
|
||||||
|
ssr::macos::prefs_domains() {
|
||||||
|
local -a list=("${SSR_MACOS_PREF_DOMAINS_DEFAULT[@]}")
|
||||||
|
if [[ -n "${SSR_MACOS_PREF_DOMAINS:-}" ]]; then
|
||||||
|
# Full override: space-separated string from config.
|
||||||
|
read -ra list <<< "$SSR_MACOS_PREF_DOMAINS"
|
||||||
|
fi
|
||||||
|
if [[ -n "${SSR_MACOS_PREF_DOMAINS_EXTRA:-}" ]]; then
|
||||||
|
# Append-only: space-separated string from config.
|
||||||
|
local extra
|
||||||
|
read -ra extra <<< "$SSR_MACOS_PREF_DOMAINS_EXTRA"
|
||||||
|
list+=("${extra[@]}")
|
||||||
|
fi
|
||||||
|
printf '%s\n' "${list[@]}" | sort -u
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Export (sync side)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Export all configured domains to <backup_dir>/macos-prefs/<domain>.plist.
|
||||||
|
# Each domain is exported independently; failures are logged and skipped.
|
||||||
|
ssr::macos::prefs_export() {
|
||||||
|
local backup_dir="$1"
|
||||||
|
local prefs_dir="$backup_dir/macos-prefs"
|
||||||
|
ssr::ensure_dir "$prefs_dir"
|
||||||
|
|
||||||
|
local domain out exported=0 failed=0
|
||||||
|
while IFS= read -r domain; do
|
||||||
|
[[ -n "$domain" ]] || continue
|
||||||
|
out="$prefs_dir/${domain}.plist"
|
||||||
|
# `defaults export` writes a binary plist to the given path.
|
||||||
|
# Redirect stdout away; errors go to stderr.
|
||||||
|
if defaults export "$domain" "$out" 2>/dev/null; then
|
||||||
|
exported=$(( exported + 1 ))
|
||||||
|
else
|
||||||
|
ssr::warn "prefs-export: could not export domain '$domain' (skipped)"
|
||||||
|
failed=$(( failed + 1 ))
|
||||||
|
fi
|
||||||
|
done < <(ssr::macos::prefs_domains)
|
||||||
|
|
||||||
|
ssr::ok "Snapshotted $exported macOS pref domain(s) → $prefs_dir"
|
||||||
|
[[ $failed -eq 0 ]] || ssr::warn "$failed domain(s) could not be exported."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Internal helpers for the restore side
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Parse the declarative defaults script and return unique domain names that
|
||||||
|
# it writes to (one per line, sorted).
|
||||||
|
# Handles both `defaults write DOMAIN …` and `sudo defaults write DOMAIN …`.
|
||||||
|
ssr::macos::_decl_domains() {
|
||||||
|
local script="$1"
|
||||||
|
[[ -f "$script" ]] || return 0
|
||||||
|
# Match both:
|
||||||
|
# defaults write DOMAIN …
|
||||||
|
# run defaults write DOMAIN …
|
||||||
|
# sudo defaults write DOMAIN …
|
||||||
|
grep -hE '(^|[[:space:]])defaults[[:space:]]+write[[:space:]]+' "$script" \
|
||||||
|
| awk '{
|
||||||
|
for (i = 1; i <= NF; i++) {
|
||||||
|
if ($i == "write") { print $(i+1); break }
|
||||||
|
}
|
||||||
|
}' \
|
||||||
|
| grep -v '^#' \
|
||||||
|
| sort -u
|
||||||
|
}
|
||||||
|
|
||||||
|
# Interactive or batch conflict resolution for one domain.
|
||||||
|
# Returns 0 = Accept (snapshot will be imported), 1 = Reject (skip import).
|
||||||
|
# Respects:
|
||||||
|
# SSR_ASSUME_YES=true → always accept without prompting
|
||||||
|
# non-TTY (stdin is not a terminal) → use SSR_PREFS_CONFLICT_DEFAULT
|
||||||
|
# SSR_PREFS_CONFLICT_DEFAULT=reject → default action when batch/non-TTY
|
||||||
|
ssr::macos::_prompt_conflict() {
|
||||||
|
local domain="$1"
|
||||||
|
local snap_ts="${2:-unknown}"
|
||||||
|
|
||||||
|
# Honour SSR_ASSUME_YES.
|
||||||
|
if [[ "${SSR_ASSUME_YES:-false}" == "true" ]]; then
|
||||||
|
ssr::log " Accepted snapshot for $domain (SSR_ASSUME_YES)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Non-interactive fallback.
|
||||||
|
if [[ ! -t 0 ]]; then
|
||||||
|
local default="${SSR_PREFS_CONFLICT_DEFAULT:-accept}"
|
||||||
|
if [[ "$default" == "reject" ]]; then
|
||||||
|
ssr::warn " Non-TTY: rejected snapshot for $domain (SSR_PREFS_CONFLICT_DEFAULT=reject)"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
ssr::log " Non-TTY: accepted snapshot for $domain (SSR_PREFS_CONFLICT_DEFAULT=accept)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Interactive prompt.
|
||||||
|
printf '\n'
|
||||||
|
ssr::warn "Domain \`%s\` — snapshot (from %s) will OVERRIDE values set by macos-defaults.sh." \
|
||||||
|
"$domain" "$snap_ts"
|
||||||
|
local reply
|
||||||
|
read -r -n 1 -p " [A]ccept snapshot / [r]eject (keep declarative)? [A/r]: " reply || reply=""
|
||||||
|
printf '\n'
|
||||||
|
case "$reply" in
|
||||||
|
r|R)
|
||||||
|
ssr::ok " Rejected snapshot for $domain; declarative values kept."
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
ssr::ok " Accepted snapshot for $domain."
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Import (restore side)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Orchestrate the import:
|
||||||
|
# 1. Compute conflicts with the declarative script.
|
||||||
|
# 2. Warn about overlapping domains and prompt per domain.
|
||||||
|
# 3. Run defaults import for each accepted domain.
|
||||||
|
# 4. Restart affected macOS services once.
|
||||||
|
#
|
||||||
|
# The declarative config/macos-defaults.sh runs *before* this function is
|
||||||
|
# called (in bin/ssr-restore), so the snapshot is the last writer and
|
||||||
|
# "wins" for accepted domains.
|
||||||
|
ssr::macos::prefs_import() {
|
||||||
|
local backup_dir="$1"
|
||||||
|
local prefs_dir="$backup_dir/macos-prefs"
|
||||||
|
|
||||||
|
if [[ ! -d "$prefs_dir" ]] || ! ls "$prefs_dir"/*.plist >/dev/null 2>&1; then
|
||||||
|
ssr::warn "No macOS pref snapshot found in $prefs_dir — skipping import."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine snapshot timestamp from the directory mtime.
|
||||||
|
local snap_ts
|
||||||
|
snap_ts="$(date -r "$prefs_dir" '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null || echo 'unknown')"
|
||||||
|
|
||||||
|
# Domains we have a snapshot for.
|
||||||
|
local -a snap_domains=()
|
||||||
|
local f
|
||||||
|
for f in "$prefs_dir"/*.plist; do
|
||||||
|
local d
|
||||||
|
d="${f##*/}"; d="${d%.plist}"
|
||||||
|
snap_domains+=("$d")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Domains touched by the declarative script.
|
||||||
|
local decl_script="${SSR_MACOS_DEFAULTS:-$SSR_ROOT/config/macos-defaults.sh}"
|
||||||
|
local decl_domains
|
||||||
|
decl_domains="$(ssr::macos::_decl_domains "$decl_script" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
# Find overlapping domains (snapshot ∩ declarative).
|
||||||
|
local -a conflicts=()
|
||||||
|
local d
|
||||||
|
for d in "${snap_domains[@]}"; do
|
||||||
|
if printf '%s\n' "$decl_domains" | grep -qFx "$d"; then
|
||||||
|
conflicts+=("$d")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#conflicts[@]} -gt 0 ]]; then
|
||||||
|
ssr::warn "Pref-snapshot overlaps with declarative defaults for ${#conflicts[@]} domain(s):"
|
||||||
|
ssr::warn " $(printf '%s, ' "${conflicts[@]}" | sed 's/, $//')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve conflicts interactively; build the final accepted list.
|
||||||
|
local -a rejected=()
|
||||||
|
for d in "${conflicts[@]}"; do
|
||||||
|
ssr::macos::_prompt_conflict "$d" "$snap_ts" || rejected+=("$d")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Import all snapshot domains except rejected ones.
|
||||||
|
local imported=0 skipped=0
|
||||||
|
for d in "${snap_domains[@]}"; do
|
||||||
|
local plist="$prefs_dir/${d}.plist"
|
||||||
|
# Check if this domain was rejected.
|
||||||
|
local is_rejected=false
|
||||||
|
local r
|
||||||
|
for r in "${rejected[@]}"; do
|
||||||
|
[[ "$r" == "$d" ]] && is_rejected=true && break
|
||||||
|
done
|
||||||
|
if $is_rejected; then
|
||||||
|
skipped=$(( skipped + 1 ))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if defaults import "$d" "$plist" 2>/dev/null; then
|
||||||
|
imported=$(( imported + 1 ))
|
||||||
|
else
|
||||||
|
ssr::warn "prefs-import: could not import domain '$d' (skipped)"
|
||||||
|
skipped=$(( skipped + 1 ))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
ssr::ok "Imported $imported macOS pref domain(s)${skipped:+ ($skipped skipped)}."
|
||||||
|
|
||||||
|
# Restart affected services once so changes take effect.
|
||||||
|
local svc
|
||||||
|
for svc in Dock Finder SystemUIServer cfprefsd; do
|
||||||
|
killall "$svc" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
ssr::ok "Restarted Dock, Finder, SystemUIServer, cfprefsd."
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user