From bf71fa61749e19e87342889f56d60ee2eecb8723 Mon Sep 17 00:00:00 2001 From: Tri Vo Khac Date: Thu, 16 Jul 2026 17:44:43 +0700 Subject: [PATCH] feat(swiftbar): add Homebrew updates plugin --- swiftbar/.config/swiftbar/brew.1d.zsh | 320 ++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100755 swiftbar/.config/swiftbar/brew.1d.zsh diff --git a/swiftbar/.config/swiftbar/brew.1d.zsh b/swiftbar/.config/swiftbar/brew.1d.zsh new file mode 100755 index 0000000..54dc90d --- /dev/null +++ b/swiftbar/.config/swiftbar/brew.1d.zsh @@ -0,0 +1,320 @@ +#!/usr/bin/env zsh + +# Homebrew Updates +# v1.0.0 +# trivk +# Checks Homebrew daily and offers manual or automatic upgrades. +# zsh,brew + +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