feat(swiftbar): add Homebrew updates plugin

This commit is contained in:
tri committed 2026-07-16 17:44:43 +07:00
1 parent e78076944a
commit bf71fa6174
1 file changed
+320
+320
View File
@@ -0,0 +1,320 @@
#!/usr/bin/env zsh
# <xbar.title>Homebrew Updates</xbar.title>
# <xbar.version>v1.0.0</xbar.version>
# <xbar.author>trivk</xbar.author>
# <xbar.desc>Checks Homebrew daily and offers manual or automatic upgrades.</xbar.desc>
# <xbar.dependencies>zsh,brew</xbar.dependencies>
set -u
PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
export HOMEBREW_NO_UPGRADE_AUTO_UPDATES_CASKS=1
PLUGIN_PATH="${0:A}"
BREW_BIN="${BREW_BIN:-$(command -v brew 2>/dev/null || true)}"
CACHE_DIR="${HOME}/Library/Caches/SwiftBarBrewUpdates"
STATE_DIR="${HOME}/Library/Application Support/SwiftBarBrewUpdates"
FORMULAE_FILE="${CACHE_DIR}/outdated-formulae"
CASKS_FILE="${CACHE_DIR}/outdated-casks"
CHECKED_AT_FILE="${CACHE_DIR}/checked-at"
ERROR_FILE="${CACHE_DIR}/last-error"
MANUAL_CHECK_FILE="${CACHE_DIR}/manual-check-running"
AUTO_FILE="${STATE_DIR}/auto-upgrade-enabled"
LOG_FILE="${STATE_DIR}/upgrade.log"
LOCK_DIR="${CACHE_DIR}/check.lock"
CHECK_INTERVAL_SECONDS=86400
mkdir -p "${CACHE_DIR}" "${STATE_DIR}"
escape_swiftbar() {
printf '%s' "$1" | sed 's/|/\\|/g'
}
escape_attribute() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/|/\\|/g'
}
is_auto_enabled() {
[ -f "${AUTO_FILE}" ]
}
is_manual_check_running() {
[ -f "${MANUAL_CHECK_FILE}" ]
}
refresh_plugin() {
plugin_name="${PLUGIN_PATH:t}"
/usr/bin/open -g "swiftbar://refreshplugin?name=${plugin_name}" >/dev/null 2>&1 || true
}
start_manual_check() {
is_manual_check_running && return 0
touch "${MANUAL_CHECK_FILE}"
nohup "${PLUGIN_PATH}" --check-now-worker >/dev/null 2>&1 &
}
run_manual_check_worker() {
rm -f "${CHECKED_AT_FILE}"
perform_daily_check
check_status=$?
rm -f "${MANUAL_CHECK_FILE}"
refresh_plugin
return "${check_status}"
}
write_error() {
printf '%s\n' "$1" > "${ERROR_FILE}"
}
clear_error() {
rm -f "${ERROR_FILE}"
}
record_check_time() {
date +%s > "${CHECKED_AT_FILE}"
}
collect_outdated() {
if ! "${BREW_BIN}" outdated --formula --quiet > "${FORMULAE_FILE}.tmp" 2> "${ERROR_FILE}.tmp"; then
write_error "$(tail -n 1 "${ERROR_FILE}.tmp")"
rm -f "${FORMULAE_FILE}.tmp" "${ERROR_FILE}.tmp"
return 1
fi
if ! "${BREW_BIN}" outdated --cask --quiet > "${CASKS_FILE}.tmp" 2> "${ERROR_FILE}.tmp"; then
write_error "$(tail -n 1 "${ERROR_FILE}.tmp")"
rm -f "${FORMULAE_FILE}.tmp" "${CASKS_FILE}.tmp" "${ERROR_FILE}.tmp"
return 1
fi
mv "${FORMULAE_FILE}.tmp" "${FORMULAE_FILE}"
mv "${CASKS_FILE}.tmp" "${CASKS_FILE}"
rm -f "${ERROR_FILE}.tmp"
clear_error
}
run_upgrade_all() {
upgrade_mode="${1:-manual}"
{
printf '\n[%s] Starting Homebrew upgrade\n' "$(date '+%Y-%m-%d %H:%M:%S')"
if [ "${upgrade_mode}" = "auto" ]; then
"${BREW_BIN}" upgrade --yes
else
"${BREW_BIN}" update && "${BREW_BIN}" upgrade --yes
fi
} >> "${LOG_FILE}" 2>&1
upgrade_status=$?
collect_outdated || true
record_check_time
return "${upgrade_status}"
}
run_upgrade_one() {
package_name="$1"
package_type="$2"
case "${package_name}" in
*[!A-Za-z0-9@+._/-]*|'')
printf 'Invalid Homebrew package name: %s\n' "${package_name}" >&2
exit 2
;;
esac
case "${package_type}" in
formula) type_flag="--formula" ;;
cask) type_flag="--cask" ;;
*)
printf 'Invalid package type: %s\n' "${package_type}" >&2
exit 2
;;
esac
{
printf '\n[%s] Upgrading %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "${package_name}"
"${BREW_BIN}" upgrade --yes "${type_flag}" "${package_name}"
} >> "${LOG_FILE}" 2>&1
upgrade_status=$?
collect_outdated || true
record_check_time
return "${upgrade_status}"
}
perform_daily_check() {
if ! mkdir "${LOCK_DIR}" 2>/dev/null; then
return 0
fi
trap 'rmdir "${LOCK_DIR}" 2>/dev/null || true' EXIT
update_output="$("${BREW_BIN}" update 2>&1)"
update_status=$?
if [ "${update_status}" -ne 0 ]; then
write_error "$(printf '%s\n' "${update_output}" | tail -n 1)"
record_check_time
return 1
fi
collect_outdated || {
record_check_time
return 1
}
record_check_time
if is_auto_enabled && [ "$(outdated_count)" -gt 0 ]; then
run_upgrade_all auto
fi
}
check_is_due() {
[ ! -f "${CHECKED_AT_FILE}" ] && return 0
checked_at="$(cat "${CHECKED_AT_FILE}" 2>/dev/null || printf '0')"
case "${checked_at}" in
*[!0-9]*|'') return 0 ;;
esac
now="$(date +%s)"
[ $((now - checked_at)) -ge "${CHECK_INTERVAL_SECONDS}" ]
}
file_line_count() {
if [ -s "$1" ]; then
wc -l < "$1" | tr -d ' '
else
printf '0'
fi
}
outdated_count() {
formula_count="$(file_line_count "${FORMULAE_FILE}")"
cask_count="$(file_line_count "${CASKS_FILE}")"
printf '%s' $((formula_count + cask_count))
}
format_checked_at() {
if [ ! -f "${CHECKED_AT_FILE}" ]; then
printf 'Never'
return
fi
checked_at="$(cat "${CHECKED_AT_FILE}")"
date -r "${checked_at}" '+%Y-%m-%d %H:%M' 2>/dev/null || printf 'Unknown'
}
render_package_menu() {
package_file="$1"
package_type="$2"
heading="$3"
[ -s "${package_file}" ] || return 0
script_path="$(escape_attribute "${PLUGIN_PATH}")"
if [ "${package_type}" = "formula" ]; then
heading_symbol="shippingbox"
package_symbol="cube.box"
else
heading_symbol="macwindow"
package_symbol="app"
fi
printf '%s | sfimage=%s\n' "${heading}" "${heading_symbol}"
while IFS= read -r package_name; do
[ -n "${package_name}" ] || continue
safe_name="$(escape_swiftbar "${package_name}")"
printf -- '--%s | sfimage=%s bash="%s" param1=--upgrade-one param2=%s param3=%s terminal=false refresh=true\n' \
"${safe_name}" "${package_symbol}" "${script_path}" "${safe_name}" "${package_type}"
done < "${package_file}"
}
render_menu() {
count="$(outdated_count)"
checked_at="$(format_checked_at)"
script_path="$(escape_attribute "${PLUGIN_PATH}")"
if is_manual_check_running; then
printf '… | sfimage=arrow.triangle.2.circlepath sfcolor=#007AFF sfsize=14 tooltip="Checking Homebrew updates…"\n'
elif [ "${count}" -eq 0 ]; then
printf '0 | sfimage=shippingbox.fill sfcolor=#34C759 sfsize=14 tooltip="Homebrew is up to date"\n'
else
printf '%s | sfimage=shippingbox.fill sfcolor=#FF9500 sfsize=14 tooltip="%s Homebrew package(s) outdated"\n' "${count}" "${count}"
fi
printf -- '---\n'
printf 'Homebrew Updates | sfimage=shippingbox.fill sfcolor=#007AFF size=14\n'
printf -- '--Outdated: %s | sfimage=number.circle\n' "${count}"
printf -- '--Last check: %s | sfimage=clock\n' "${checked_at}"
if [ -s "${ERROR_FILE}" ]; then
error_message="$(escape_swiftbar "$(tail -n 1 "${ERROR_FILE}")")"
printf -- '--Last check failed: %s | sfimage=exclamationmark.triangle.fill sfcolor=#FF3B30 color=#FF3B30\n' "${error_message}"
fi
printf -- '---\n'
if is_auto_enabled; then
printf 'Auto upgrade | checked=true sfimage=arrow.triangle.2.circlepath sfcolor=#34C759 bash="%s" param1=--disable-auto terminal=false refresh=true\n' "${script_path}"
else
printf 'Auto upgrade | checked=false sfimage=arrow.triangle.2.circlepath sfcolor=#8E8E93 bash="%s" param1=--enable-auto terminal=false refresh=true\n' "${script_path}"
fi
if is_manual_check_running; then
printf 'Check for updates now | sfimage=arrow.clockwise disabled=true\n'
else
printf 'Check for updates now | sfimage=arrow.clockwise bash="%s" param1=--check-now terminal=false refresh=true\n' "${script_path}"
fi
if [ "${count}" -gt 0 ]; then
printf 'Upgrade all in background | sfimage=arrow.up sfcolor=#007AFF bash="%s" param1=--upgrade-all terminal=false refresh=true\n' "${script_path}"
printf -- '---\n'
render_package_menu "${FORMULAE_FILE}" formula 'Formulae'
render_package_menu "${CASKS_FILE}" cask 'Casks'
fi
if [ -f "${LOG_FILE}" ]; then
log_path="$(escape_attribute "${LOG_FILE}")"
printf -- '---\n'
printf 'Open upgrade log | sfimage=doc.text bash=/usr/bin/open param1="%s" terminal=false\n' "${log_path}"
fi
}
if [ -z "${BREW_BIN}" ]; then
printf '! | sfimage=exclamationmark.triangle.fill sfcolor=#FF3B30 color=#FF3B30\n'
printf -- '---\nHomebrew was not found in /opt/homebrew or /usr/local.\n'
exit 0
fi
case "${1:-}" in
--enable-auto)
touch "${AUTO_FILE}"
exit 0
;;
--disable-auto)
rm -f "${AUTO_FILE}"
exit 0
;;
--check-now)
start_manual_check
exit $?
;;
--check-now-worker)
run_manual_check_worker
exit $?
;;
--upgrade-all)
run_upgrade_all
exit $?
;;
--upgrade-one)
run_upgrade_one "${2:-}" "${3:-}"
exit $?
;;
esac
if check_is_due; then
perform_daily_check || true
fi
render_menu