feat(swiftbar): add Homebrew updates plugin
This commit is contained in:
1 parent
e78076944a
commit
bf71fa6174
1 file changed
+320
Executable
+320
@@ -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
|
||||||
Reference in new issue
Block a user