Skip to content

Instantly share code, notes, and snippets.

@jonshaffer
Last active December 6, 2025 12:34
Show Gist options
  • Select an option

  • Save jonshaffer/62c857cc7fc688747f2fa2a02baafa10 to your computer and use it in GitHub Desktop.

Select an option

Save jonshaffer/62c857cc7fc688747f2fa2a02baafa10 to your computer and use it in GitHub Desktop.
nix darwin manager swiftbar plugin
# Ignore log directories and files
*/.logs/
*/logs/
*.log
# Ignore temporary and cache files
*.tmp
*.swp
.DS_Store
*/.DS_Store
.*cache*
.nix-*
# Ignore PID files
*.pid
# Ignore disabled plugins
*.disabled
# Ignore documentation files (SwiftBar only needs executable scripts)
*.md
*.txt
README*
**/README.md
README.md
# Ignore library files and directories
lib/
*.lib.sh
config.sh
# Ignore the ignore file itself
.swiftbarignore

nix-darwin-manager

file structure:

swiftbar/
  .README.md
  .swiftbarignore
  scripts/ # <- configure swiftbar plugin directory to here
    .swiftbarignore
    lib/
      common.sh
    nix-darwin-manager/
      nix-darwin-manager.5s.sh
      .logs/
        .last-error-status
        .nix-store-size-cache
        nix-darwin-20251105_143517.log
#!/bin/bash
#
# Common utility functions for SwiftBar plugins
# This file should be sourced by plugin scripts
#
# ============================================================================
# LOGGING FUNCTIONS
# ============================================================================
# Log informational message
log_info() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $*" >&2
}
# Log error message
log_error() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
}
# Log warning message
log_warn() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] WARN: $*" >&2
}
# ============================================================================
# NOTIFICATION FUNCTIONS
# ============================================================================
# Send macOS notification
# Usage: notify "Title" "Message" ["sound_name"]
notify() {
local title="$1"
local message="$2"
local sound="${3:-default}"
osascript -e "display notification \"$message\" with title \"$title\" sound name \"$sound\"" 2>/dev/null
}
# Send error notification
notify_error() {
notify "Error" "$1" "Basso"
}
# Send success notification
notify_success() {
notify "Success" "$1" "Glass"
}
# ============================================================================
# FILE OPERATIONS
# ============================================================================
# Safely write to file with atomic operation
# Usage: safe_write "file_path" "content"
safe_write() {
local file="$1"
local content="$2"
local tmp="${file}.tmp.$$"
if echo "$content" > "$tmp" 2>/dev/null; then
mv "$tmp" "$file" 2>/dev/null && return 0
fi
rm -f "$tmp" 2>/dev/null
return 1
}
# Check if file is writable, create if needed
# Usage: ensure_writable "file_path"
ensure_writable() {
local file="$1"
local dir
dir=$(dirname "$file")
# Ensure directory exists
if [[ ! -d "$dir" ]]; then
mkdir -p "$dir" 2>/dev/null || return 1
fi
# Create file if it doesn't exist
if [[ ! -f "$file" ]]; then
touch "$file" 2>/dev/null || return 1
fi
# Check if writable
[[ -w "$file" ]]
}
# ============================================================================
# PID FILE MANAGEMENT
# ============================================================================
# Create PID file
# Usage: create_pid_file "pid_file" "pid"
create_pid_file() {
local pid_file="$1"
local pid="$2"
safe_write "$pid_file" "$pid"
}
# Remove PID file
# Usage: remove_pid_file "pid_file"
remove_pid_file() {
local pid_file="$1"
[[ -f "$pid_file" ]] && rm -f "$pid_file" 2>/dev/null
}
# Check if process is running
# Usage: is_process_running "pid"
is_process_running() {
local pid="$1"
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
}
# Get PID from file and check if running
# Usage: get_running_pid "pid_file"
# Returns: PID if running, empty if not
get_running_pid() {
local pid_file="$1"
if [[ -f "$pid_file" ]]; then
local pid
pid=$(cat "$pid_file" 2>/dev/null)
if is_process_running "$pid"; then
echo "$pid"
return 0
else
# Clean up stale PID file
remove_pid_file "$pid_file"
fi
fi
return 1
}
# ============================================================================
# INPUT VALIDATION
# ============================================================================
# Validate that a value is a positive integer
# Usage: is_positive_integer "value"
is_positive_integer() {
local value="$1"
[[ "$value" =~ ^[0-9]+$ ]] && [[ "$value" -gt 0 ]]
}
# Sanitize input by removing dangerous characters
# Usage: sanitize_input "value"
sanitize_input() {
local input="$1"
# Remove shell metacharacters
echo "$input" | tr -d '\n;|&$`<>(){}[]!*?~'
}
# Validate path is within allowed directory
# Usage: validate_path "path" "allowed_base"
validate_path() {
local path="$1"
local allowed_base="$2"
# Resolve to absolute path
local resolved
resolved=$(cd "$(dirname "$path")" 2>/dev/null && pwd -P)/$(basename "$path") || return 1
# Check if it starts with allowed base
[[ "$resolved" == "$allowed_base"* ]]
}
# ============================================================================
# CACHE MANAGEMENT
# ============================================================================
# Check if cache is valid (not expired)
# Usage: is_cache_valid "cache_file" "ttl_seconds"
is_cache_valid() {
local cache_file="$1"
local ttl="${2:-3600}" # Default 1 hour
if [[ -f "$cache_file" ]]; then
local mtime=$(stat -c "%Y" "$cache_file" 2>/dev/null || stat -f "%m" "$cache_file" 2>/dev/null || echo 0)
local cache_age=$(($(date +%s) - mtime))
[[ $cache_age -lt $ttl ]]
else
return 1
fi
}
# Read cache if valid, otherwise run command and cache result
# Usage: get_cached "cache_file" "ttl_seconds" "command to run"
get_cached() {
local cache_file="$1"
local ttl="$2"
shift 2
local command=("$@")
if is_cache_valid "$cache_file" "$ttl"; then
cat "$cache_file" 2>/dev/null
else
local result
result=$("${command[@]}" 2>/dev/null) || return 1
safe_write "$cache_file" "$result"
echo "$result"
fi
}
# ============================================================================
# LOG ROTATION
# ============================================================================
# Clean up old log files
# Usage: cleanup_old_logs "log_dir" "retention_days" "max_count" "pattern"
cleanup_old_logs() {
local log_dir="$1"
local retention_days="${2:-30}"
local max_count="${3:-100}"
local pattern="${4:-*.log}"
[[ ! -d "$log_dir" ]] && return 0
# Delete logs older than retention period
find "$log_dir" -name "$pattern" -type f -mtime "+${retention_days}" -delete 2>/dev/null
# Keep only N most recent logs
local log_count
log_count=$(find "$log_dir" -name "$pattern" -type f 2>/dev/null | wc -l | tr -d ' ')
if [[ $log_count -gt $max_count ]]; then
# Get oldest logs beyond max_count and delete them
find "$log_dir" -name "$pattern" -type f -print0 2>/dev/null | \
xargs -0 ls -t | \
tail -n "+$((max_count + 1))" | \
xargs rm -f 2>/dev/null
fi
}
# ============================================================================
# DEPENDENCY CHECKING
# ============================================================================
# Check if all required commands are available
# Usage: check_dependencies "cmd1" "cmd2" "cmd3"
# Returns: 0 if all available, 1 if any missing (prints missing to stderr)
check_dependencies() {
local missing_deps=()
for cmd in "$@"; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing_deps+=("$cmd")
fi
done
if [[ ${#missing_deps[@]} -gt 0 ]]; then
log_error "Missing dependencies: ${missing_deps[*]}"
return 1
fi
return 0
}
# ============================================================================
# CONFIRMATION DIALOGS
# ============================================================================
# Show confirmation dialog
# Usage: confirm "Question text" "Button text"
# Returns: 0 if confirmed, 1 if cancelled
confirm() {
local question="$1"
local button="${2:-OK}"
local response
response=$(osascript -e "display dialog \"$question\" buttons {\"Cancel\", \"$button\"} default button \"Cancel\" with icon caution" 2>/dev/null)
[[ "$response" =~ "$button" ]]
}
# ============================================================================
# ERROR HANDLING
# ============================================================================
# Exit with error message
# Usage: die "error message" [exit_code]
die() {
local msg="$1"
local code="${2:-1}"
log_error "$msg"
notify_error "$msg"
exit "$code"
}
# Run command with error handling
# Usage: run_or_die "error message" command args...
run_or_die() {
local error_msg="$1"
shift
if ! "$@"; then
die "$error_msg" 1
fi
}
# ============================================================================
# UTILITY FUNCTIONS
# ============================================================================
# Get human-readable size from bytes
# Usage: human_readable_size "bytes"
human_readable_size() {
local bytes="$1"
if command -v numfmt >/dev/null 2>&1; then
numfmt --to=iec-i --suffix=B "$bytes" 2>/dev/null
else
# Fallback for systems without numfmt
awk -v bytes="$bytes" 'BEGIN {
split("B KiB MiB GiB TiB", units)
for (i=5; i>0; i--) {
if (bytes >= 1024^(i-1)) {
printf "%.1f %s\n", bytes/(1024^(i-1)), units[i]
exit
}
}
}'
fi
}
# Check if script is running in SwiftBar
# Usage: is_swiftbar
is_swiftbar() {
[[ -n "${SWIFTBAR:-}" ]] || [[ -n "${BitBar:-}" ]]
}
#!/bin/bash
# <xbar.title>Nix Darwin Manager</xbar.title>
# <xbar.version>v1.1</xbar.version>
# <xbar.author>Jon Shaffer</xbar.author>
# <xbar.author.github>jonshaffer</xbar.author.github>
# <xbar.desc>Manage nix-darwin configuration</xbar.desc>
# <xbar.dependencies>nix,nix-darwin</xbar.dependencies>
# ============================================================================
# CONFIGURATION
# ============================================================================
NIX_DARWIN_PATH=$(realpath /etc/nix-darwin)
CONFIGURATION=$(hostname -s)
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Optional: auto-update all flake inputs before a normal Rebuild (default: false)
AUTO_UPDATE_ON_REBUILD=${AUTO_UPDATE_ON_REBUILD:-false}
# Log retention settings
LOG_RETENTION_DAYS=${LOG_RETENTION_DAYS:-30}
MAX_LOG_FILES=${MAX_LOG_FILES:-100}
# Ensure .logs directory exists with restrictive permissions
mkdir -p "${SCRIPT_DIR}/.logs"
chmod 700 "${SCRIPT_DIR}/.logs" 2>/dev/null || true
LOG_FILE="${SCRIPT_DIR}/.logs/nix-darwin-${TIMESTAMP}.log"
REBUILD_FLAG="${SCRIPT_DIR}/.logs/.nix-darwin-rebuilding"
REBUILD_PID_FILE="${SCRIPT_DIR}/.logs/.rebuild.pid"
# Prefer SwiftBar's per-plugin cache directory when available
STORE_SIZE_DIR="${SWIFTBAR_PLUGIN_CACHE_PATH:-${SCRIPT_DIR}/.logs}"
mkdir -p "${STORE_SIZE_DIR}"
STORE_SIZE_CACHE="${STORE_SIZE_DIR}/.nix-store-size-cache"
STORE_SIZE_LOCK="${STORE_SIZE_DIR}/.nix-store-size.lock"
CACHE_TTL=3600 # 1 hour in seconds
# Error status cache for performance
ERROR_STATUS_CACHE="${SCRIPT_DIR}/.logs/.last-error-status"
# ----------------------------------------------------------------------------
# Updates checking (flake inputs + Homebrew)
# ----------------------------------------------------------------------------
UPDATES_TTL=${UPDATES_TTL:-21600} # 6 hours
UPDATES_DIR="${SWIFTBAR_PLUGIN_CACHE_PATH:-${SCRIPT_DIR}/.logs}"
# Flake inputs updates cache
FLAKE_UPDATES_CACHE="${UPDATES_DIR}/.flake-updates.json"
FLAKE_UPDATES_LOCK="${UPDATES_DIR}/.flake-updates.lock"
# Homebrew updates cache
BREW_UPDATES_CACHE="${UPDATES_DIR}/.brew-updates.json"
BREW_UPDATES_LOCK="${UPDATES_DIR}/.brew-updates.lock"
# ============================================================================
# LOAD SHARED UTILITIES
# ============================================================================
# Source common utility functions
source "$(dirname "${BASH_SOURCE[0]}")/../lib/common.sh"
# ============================================================================
# DEPENDENCY CHECKING
# ============================================================================
check_dependencies() {
local missing_deps=()
for cmd in nix git; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing_deps+=("$cmd")
fi
done
if [[ ${#missing_deps[@]} -gt 0 ]]; then
echo "⚠️"
echo "---"
echo "Missing dependencies: ${missing_deps[*]} | color=#ff0000"
echo "---"
echo "Please install missing dependencies to use this plugin"
exit 0
fi
}
# ============================================================================
# CORE FUNCTIONS
# ============================================================================
# Returns success if flake directory and lockfile are writable by current user
flake_writable() {
local p="$NIX_DARWIN_PATH"
if [[ ! -d "$p" ]]; then
return 1
fi
if [[ ! -w "$p" ]]; then
return 1
fi
if [[ -e "$p/flake.lock" && ! -w "$p/flake.lock" ]]; then
return 1
fi
return 0
}
# Detached, locked, atomic cache updater for /nix/store size
update_store_size_cache_detached() {
# Prevent concurrent updates
if [[ -e "${STORE_SIZE_LOCK}" ]]; then
return
fi
: > "${STORE_SIZE_LOCK}"
nohup nice -n 19 bash -c '
set -euo pipefail
cache="$1"
lock="$2"
tmp="${cache}.tmp"
cleanup() { /bin/rm -f "$tmp" "$lock"; }
trap cleanup EXIT
if [[ -d /nix/store ]]; then
/usr/bin/du -sh /nix/store 2>/dev/null | /usr/bin/cut -f1 > "$tmp" || true
else
echo "-" > "$tmp"
fi
if [[ -s "$tmp" ]]; then
/bin/mv -f "$tmp" "$cache"
fi
' _ "${STORE_SIZE_CACHE}" "${STORE_SIZE_LOCK}" >/dev/null 2>&1 & disown
}
get_store_size() {
if [[ -f "$STORE_SIZE_CACHE" ]]; then
local now mtime cache_age cached_size
now=$(date +%s)
# Try GNU stat first, fallback to BSD stat
mtime=$(stat -c "%Y" "$STORE_SIZE_CACHE" 2>/dev/null || stat -f "%m" "$STORE_SIZE_CACHE" 2>/dev/null)
mtime=${mtime:-$now}
cache_age=$(( now - mtime ))
cached_size=$(cat "$STORE_SIZE_CACHE" 2>/dev/null)
if [[ $cache_age -gt $CACHE_TTL ]]; then
# Refresh cache asynchronously, return old value immediately
update_store_size_cache_detached
fi
if [[ -n "$cached_size" ]]; then
echo "$cached_size"
else
echo "Calculating..."
fi
else
# No cache exists - kick off detached calc and show placeholder
update_store_size_cache_detached
echo "Calculating..."
fi
}
# Check rebuild status from log with caching for performance
check_rebuild_status() {
local log_file="$1"
# Only recheck if log is newer than cache
if [[ -f "$log_file" ]] && [[ "$log_file" -nt "$ERROR_STATUS_CACHE" ]]; then
if grep -q "error:\|Rebuild failed" "$log_file" 2>/dev/null; then
echo "error" > "$ERROR_STATUS_CACHE"
else
echo "success" > "$ERROR_STATUS_CACHE"
fi
fi
cat "$ERROR_STATUS_CACHE" 2>/dev/null || echo "unknown"
}
# Get last error from log
get_last_error() {
local log_file="$1"
if [[ -f "$log_file" ]]; then
grep -E "error:|failed" "$log_file" 2>/dev/null | tail -1 | cut -c1-80
fi
}
# Commit helper for flake changes inside $NIX_DARWIN_PATH
commit_flake() {
# Only attempt commit if this is a git repo
if git -C "$NIX_DARWIN_PATH" rev-parse --show-toplevel >/dev/null 2>&1; then
local files=(
flake.nix
flake.lock
configuration.nix
home.nix
)
if [[ -d "$NIX_DARWIN_PATH/overlays" ]]; then
# Add all overlay .nix files relative to repo root
while IFS= read -r f; do
files+=("${f#"$NIX_DARWIN_PATH/"}")
done < <(find "$NIX_DARWIN_PATH/overlays" -type f -name '*.nix' 2>/dev/null)
fi
# Use sudo if the flake is not writable by current user
local git_cmd=(git -C "$NIX_DARWIN_PATH")
if ! flake_writable; then
git_cmd=(sudo "${git_cmd[@]}")
fi
"${git_cmd[@]}" add "${files[@]}" 2>/dev/null || true
if ! "${git_cmd[@]}" diff --cached --quiet; then
local msg="chore(nix-darwin): auto-update/auto-save ($(date +'%Y-%m-%dT%H:%M:%S%z'))"
"${git_cmd[@]}" commit -m "$msg" --no-verify || true
fi
fi
}
# Run a command and only keep a log file if it has output
run_and_maybe_log() {
local tmp_log="${LOG_FILE}.tmp"
"$@" >"$tmp_log" 2>&1
local exit_code=$?
if [[ -s "$tmp_log" ]]; then
mv "$tmp_log" "$LOG_FILE"
else
rm -f "$tmp_log"
fi
return $exit_code
}
# ============================================================================
# ACTION HANDLERS
# ============================================================================
if [[ "$1" == "open-config" ]]; then
run_and_maybe_log code "$NIX_DARWIN_PATH"
exit 0
elif [[ "$1" == "rebuild" ]]; then
# Create rebuild flag file
touch "$REBUILD_FLAG"
# Commit any pending local changes before rebuild
commit_flake
if [[ "$AUTO_UPDATE_ON_REBUILD" == "true" ]]; then
echo "Updating all flake inputs before rebuild..." | tee -a "$LOG_FILE"
if flake_writable; then
(cd "$NIX_DARWIN_PATH" && nix flake update) 2>&1 | tee -a "$LOG_FILE" || true
else
echo "No write access to flake; using sudo for update" | tee -a "$LOG_FILE"
(cd "$NIX_DARWIN_PATH" && sudo -H nix flake update) 2>&1 | tee -a "$LOG_FILE" || true
fi
commit_flake
fi
echo "Starting nix-darwin rebuild at $(date)" | tee "$LOG_FILE"
echo "🔄 Building system configuration... (output will stream below)"
echo "======================================================"
# Stream output to both terminal and log file in real-time
sudo -H nix run nix-darwin/master#darwin-rebuild -- switch --flake "path:$NIX_DARWIN_PATH#$CONFIGURATION" --show-trace 2>&1 | tee -a "$LOG_FILE" &
# Store PID for potential cancellation
REBUILD_PID=$!
echo "$REBUILD_PID" > "$REBUILD_PID_FILE"
# Wait for rebuild to complete
wait "$REBUILD_PID"
EXIT_CODE=$?
# Cleanup
rm -f "$REBUILD_PID_FILE"
rm -f "$REBUILD_FLAG"
echo "======================================================"
if [[ $EXIT_CODE -eq 0 ]]; then
echo "Rebuild completed successfully at $(date)" | tee -a "$LOG_FILE"
# Update store size cache in background after successful rebuild
update_store_size_cache_detached
# Clear error status cache
echo "success" > "$ERROR_STATUS_CACHE"
notify "Nix Darwin Manager" "Rebuild completed successfully" "Glass"
echo "✅ Rebuild completed successfully!"
else
echo "Rebuild failed at $(date)" | tee -a "$LOG_FILE"
echo "error" > "$ERROR_STATUS_CACHE"
notify "Nix Darwin Manager" "Rebuild failed - check logs" "Basso"
echo "❌ Rebuild failed! Check logs for details."
fi
elif [[ "$1" == "cancel-rebuild" ]]; then
if [[ -f "$REBUILD_PID_FILE" ]]; then
REBUILD_PID=$(cat "$REBUILD_PID_FILE" 2>/dev/null)
if is_process_running "$REBUILD_PID"; then
echo "Cancelling rebuild (PID: $REBUILD_PID)..."
sudo kill -TERM "$REBUILD_PID" 2>/dev/null || true
sleep 1
# Force kill if still running
if is_process_running "$REBUILD_PID"; then
sudo kill -9 "$REBUILD_PID" 2>/dev/null || true
fi
rm -f "$REBUILD_PID_FILE"
rm -f "$REBUILD_FLAG"
notify "Nix Darwin Manager" "Rebuild cancelled"
echo "Rebuild cancelled" >> "$LOG_FILE"
fi
fi
exit 0
elif [[ "$1" == "update-rebuild" ]]; then
# Update all flake inputs, then rebuild
touch "$REBUILD_FLAG"
echo "Starting flake update (all inputs) at $(date)" | tee "$LOG_FILE"
echo "🔄 Updating all flake inputs..." | tee -a "$LOG_FILE"
if flake_writable; then
(cd "$NIX_DARWIN_PATH" && nix flake update) 2>&1 | tee -a "$LOG_FILE"
else
echo "No write access to flake; using sudo for update" | tee -a "$LOG_FILE"
(cd "$NIX_DARWIN_PATH" && sudo -H nix flake update) 2>&1 | tee -a "$LOG_FILE"
fi
UPDATE_EXIT=${PIPESTATUS[0]}
# Commit updated lockfile/config
commit_flake
echo "======================================================" | tee -a "$LOG_FILE"
echo "🔄 Rebuilding system..." | tee -a "$LOG_FILE"
sudo -H nix run nix-darwin/master#darwin-rebuild -- switch --impure --flake "path:$NIX_DARWIN_PATH#$CONFIGURATION" --show-trace 2>&1 | tee -a "$LOG_FILE"
REBUILD_EXIT=${PIPESTATUS[0]}
rm -f "$REBUILD_FLAG"
echo "======================================================" | tee -a "$LOG_FILE"
if [[ $REBUILD_EXIT -eq 0 ]]; then
echo "success" > "$ERROR_STATUS_CACHE"
if [[ $UPDATE_EXIT -eq 0 ]]; then
notify "Nix Darwin Manager" "Update + rebuild completed" "Glass"
echo "✅ Update + rebuild completed!" | tee -a "$LOG_FILE"
else
notify "Nix Darwin Manager" "Update failed, rebuild completed"
echo "⚠️ Update failed, rebuild completed" | tee -a "$LOG_FILE"
fi
else
echo "error" > "$ERROR_STATUS_CACHE"
notify "Nix Darwin Manager" "Update + rebuild failed - check logs" "Basso"
echo "❌ Update + rebuild failed!" | tee -a "$LOG_FILE"
fi
elif [[ "$1" == "collect-garbage" ]]; then
DAYS=${2:-7} # Default to 7 days if not specified
# Validate input
if [[ "$DAYS" != "all" ]] && ! [[ "$DAYS" =~ ^[0-9]+$ ]]; then
notify "Nix Darwin Manager" "Invalid days value: $DAYS" "Basso"
exit 1
fi
# Confirm destructive operation for "all"
if [[ "$DAYS" == "all" ]]; then
if ! confirm "This will delete ALL old nix store paths. Continue?" "Delete All"; then
echo "Garbage collection cancelled"
exit 0
fi
fi
echo "Starting nix garbage collection (${DAYS} days) at $(date)" | tee "$LOG_FILE"
echo "🗑️ Cleaning up old nix store paths..."
echo "======================================================"
# Get store size before cleanup
BEFORE_SIZE=$(/usr/bin/du -sh /nix/store 2>/dev/null | /usr/bin/cut -f1)
echo "Store size before cleanup: $BEFORE_SIZE"
# Run garbage collection
if [[ "$DAYS" == "all" ]]; then
sudo nix-collect-garbage -d 2>&1 | tee -a "$LOG_FILE"
else
sudo nix-collect-garbage --delete-older-than "${DAYS}d" 2>&1 | tee -a "$LOG_FILE"
fi
EXIT_CODE=${PIPESTATUS[0]}
# Get store size after cleanup and update cache
AFTER_SIZE=$(/usr/bin/du -sh /nix/store 2>/dev/null | /usr/bin/cut -f1)
echo "$AFTER_SIZE" > "$STORE_SIZE_CACHE"
echo "======================================================"
if [[ $EXIT_CODE -eq 0 ]]; then
echo "Garbage collection completed successfully at $(date)" | tee -a "$LOG_FILE"
echo "Store size after cleanup: $AFTER_SIZE"
notify "Nix Darwin Manager" "Cleaned up nix store: $BEFORE_SIZE → $AFTER_SIZE" "Glass"
echo "✅ Garbage collection completed! $BEFORE_SIZE → $AFTER_SIZE"
else
echo "Garbage collection failed at $(date)" | tee -a "$LOG_FILE"
notify "Nix Darwin Manager" "Garbage collection failed - check logs" "Basso"
echo "❌ Garbage collection failed! Check logs for details."
fi
elif [[ "$1" == "show-logs" ]]; then
LATEST_LOG=$(ls -t "${SCRIPT_DIR}"/.logs/nix-darwin-*.log 2>/dev/null | head -1)
if [[ -n "$LATEST_LOG" ]]; then
echo "=== Latest Nix Darwin Log: $(basename "$LATEST_LOG") ==="
tail -50 "$LATEST_LOG"
else
echo "No nix-darwin logs found"
fi
exit 0
elif [[ "$1" == "open-latest-log" ]]; then
LATEST_LOG=$(ls -t "${SCRIPT_DIR}"/.logs/nix-darwin-*.log 2>/dev/null | head -1)
if [[ -n "$LATEST_LOG" ]]; then
code "$LATEST_LOG"
else
notify "Nix Darwin Manager" "No nix-darwin logs found"
fi
exit 0
elif [[ "$1" == "clear-logs" ]]; then
LOG_COUNT=$(ls -1 "${SCRIPT_DIR}"/.logs/nix-darwin-*.log 2>/dev/null | wc -l | tr -d ' ')
if [[ "$LOG_COUNT" -gt 0 ]]; then
if confirm "Delete all $LOG_COUNT log file(s)?" "Delete"; then
rm -f "${SCRIPT_DIR}"/.logs/nix-darwin-*.log 2>/dev/null || true
rm -f "$ERROR_STATUS_CACHE" 2>/dev/null || true
notify "Nix Darwin Manager" "Cleared $LOG_COUNT nix-darwin log(s)" "Glass"
fi
else
notify "Nix Darwin Manager" "No nix-darwin logs to clear"
fi
exit 0
fi
# ============================================================================
# AUTO-CLEANUP OLD LOGS ON LOAD
# ============================================================================
cleanup_old_logs "${SCRIPT_DIR}/.logs" "$LOG_RETENTION_DAYS" "$MAX_LOG_FILES" "nix-darwin-*.log"
# ============================================================================
# DEPENDENCY CHECK
# ============================================================================
check_dependencies
# ============================================================================
# MENU DISPLAY
# ============================================================================
# Check if nix-darwin config exists
if [[ -d "$NIX_DARWIN_PATH" ]]; then
CONFIG_STATUS="🟢"
CONFIG_TEXT="Config Found"
else
CONFIG_STATUS="🔴"
CONFIG_TEXT="No Config"
fi
# Check if we can run nix commands and current status
if command -v nix >/dev/null 2>&1; then
# Check if rebuild is currently in progress
if [[ -f "$REBUILD_FLAG" ]]; then
# Try to get current stage from log
LATEST_LOG=$(ls -t "${SCRIPT_DIR}"/.logs/nix-darwin-*.log 2>/dev/null | head -1)
CURRENT_STAGE=$(tail -1 "$LATEST_LOG" 2>/dev/null | grep -oE "(building|setting up|reloading|configuring)" || echo "processing")
NIX_ICON="⏳"
NIX_TEXT="Building ($CURRENT_STAGE)"
else
# Check latest log for errors with cached status
LATEST_LOG=$(ls -t "${SCRIPT_DIR}"/.logs/nix-darwin-*.log 2>/dev/null | head -1)
STATUS=$(check_rebuild_status "$LATEST_LOG")
if [[ "$STATUS" == "error" ]]; then
NIX_ICON="🕸️"
NIX_TEXT="Last build failed"
else
NIX_ICON="❄️"
NIX_TEXT="Ready"
fi
fi
else
NIX_ICON="➖"
NIX_TEXT="Nix not found"
fi
# Display menu bar icon and status
echo "$NIX_ICON"
echo "---"
echo "$CONFIG_STATUS $CONFIG_TEXT"
if [[ -n "$NIX_TEXT" ]]; then
echo "Status: $NIX_TEXT | color=#666666"
fi
# Show last error if rebuild failed
if [[ "$STATUS" == "error" ]] && [[ -n "$LATEST_LOG" ]]; then
LAST_ERROR=$(get_last_error "$LATEST_LOG")
if [[ -n "$LAST_ERROR" ]]; then
echo "---"
echo "Last Error: | color=#ff0000"
echo "--${LAST_ERROR} | color=#ff0000 font=Monaco"
fi
fi
echo "---"
echo "Open Config in VS Code | bash='$0' param1=open-config terminal=false refresh=true"
# Show cancel option if rebuild in progress
if [[ -f "$REBUILD_FLAG" ]]; then
echo "Cancel Rebuild | bash='$0' param1=cancel-rebuild terminal=false refresh=true color=#ff0000"
else
echo "Rebuild System | bash='$0' param1=rebuild terminal=false refresh=true"
fi
echo "Update + Rebuild | bash='$0' param1=update-rebuild terminal=false refresh=true"
echo "Collect Garbage (7 days) | bash='$0' param1=collect-garbage terminal=true refresh=true"
echo "Garbage Collection Options"
echo "--Quick (3 days) | bash='$0' param1=collect-garbage param2=3 terminal=true refresh=true"
echo "--Standard (7 days) | bash='$0' param1=collect-garbage param2=7 terminal=true refresh=true"
echo "--Conservative (30 days) | bash='$0' param1=collect-garbage param2=30 terminal=true refresh=true"
echo "--Nuclear (all old) ⚠️ | bash='$0' param1=collect-garbage param2=all terminal=true refresh=true color=#ff6b35"
echo "Show Recent Logs | bash='$0' param1=show-logs terminal=true"
echo "Open Latest Log | bash='$0' param1=open-latest-log terminal=false refresh=true"
echo "---"
echo "Config Path: $NIX_DARWIN_PATH | color=#666666"
# Display store size with visual warning for large sizes
STORE_SIZE=$(get_store_size)
if [[ -z "$STORE_SIZE" || "$STORE_SIZE" == "Calculating..." ]]; then
echo "Store Size: Calculating... | color=#666666"
elif [[ "$STORE_SIZE" == "-" ]]; then
echo "Store Size: Nix not installed | color=#666666"
elif [[ "$STORE_SIZE" =~ ^[0-9.]+G$ ]] && (( $(echo "${STORE_SIZE%G} > 20" | bc -l 2>/dev/null || echo "0") )); then
echo "Store Size: $STORE_SIZE ⚠️ | color=#ff6b35"
else
echo "Store Size: $STORE_SIZE | color=#666666"
fi
LATEST_LOG=$(ls -t "${SCRIPT_DIR}"/.logs/nix-darwin-*.log 2>/dev/null | head -1)
if [[ -n "$LATEST_LOG" ]]; then
# Try GNU stat, fallback to BSD stat for timestamp display
LAST_MOD=$(stat -c "%y" "$LATEST_LOG" 2>/dev/null || stat -f "%Sm" -t "%Y-%m-%d %H:%M" "$LATEST_LOG" 2>/dev/null)
LAST_MOD=$(echo "$LAST_MOD" | cut -d'.' -f1 | sed 's/ /T/' | cut -d'T' -f1-2 | tr 'T' ' ')
echo "Last Log: $LAST_MOD | color=#666666"
LOG_COUNT=$(ls -1 "${SCRIPT_DIR}"/.logs/nix-darwin-*.log 2>/dev/null | wc -l | tr -d ' ')
# Calculate total log file size
LOG_BYTES=$(find "${SCRIPT_DIR}/.logs" -type f -name 'nix-darwin-*.log' -exec stat -c "%s" {} + 2>/dev/null | awk '{s+=$1} END {print s+0}')
if [[ -z "$LOG_BYTES" || "$LOG_BYTES" == "0" ]]; then
# Fallback to BSD stat
LOG_BYTES=$(find "${SCRIPT_DIR}/.logs" -type f -name 'nix-darwin-*.log' -exec stat -f "%z" {} + 2>/dev/null | awk '{s+=$1} END {print s+0}')
fi
LOG_SIZE=$(human_readable_size "$LOG_BYTES")
echo "Log Count: ${LOG_COUNT} files • ${LOG_SIZE} | color=#666666"
echo "Clear Logs | bash='$0' param1=clear-logs terminal=false refresh=true"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment