Skip to content → Skip to footer →

Send Mac Notification When Long-Running Terminal Process Completes

Tiny shell startup script to send Mac system notification when a long-running terminal process completes.


ATOM 140 words 🥬 fresh last modified 1 day ago
🏁 mvp This note lacks refinement, but it has been completed “enough”.
☑️ terms Terms of Service
By reading, you agree to the site's Terms of Service — TL;DR: doubt and fact-check everything I've written!

I would run brew upgrade or pnpm install and just stare at the screen or find something else to burn the time. If I changed windows, I’d forget I was waiting for a terminal process.

Got AI to generate a small script. Been working great!

Worse: I had went away and checked back just to find it had failed. Update: I think it does not notify on failures. Future enhancement.

It’s really increased my productivity. or at least, perceived productivity. Either ways, I am happier to have this. Do send feedback if this improvements can be made!

[!warning] Unverified AI generated code I’m not familiar with shell. I’ve had AI add safeguards for exploits (e.g. string sanitisation) and annotated as much as possible, but I’m no expert. So no warranty from me. Will iterate and improve.

TIMEFMT=$'%J took %*E'

preexec() {
  local cmd=${1[1,500]}

  # store start time
  typeset -g cmd_start=$EPOCHSECONDS

  # sanitize command
  local cmd=${1[1,500]}
  cmd=${cmd//$'\n'/ }
  cmd=${cmd//$'\r'/ }
  cmd=${cmd//[^[:print:]]/?}
  cmd=${cmd#-}
  # truncate
  (( ${#cmd} > 60 )) && cmd="${cmd[1,57]}…"

  # ALWAYS overwrite (no stale state)
  typeset -g cmd_title=$cmd

  # flag for expected long-running commands
  typeset -g is_expected_long_running=0
  # can extend with e.g. `*node*` or *watch*
  case "$1" in
    *dev*|*start*|*serve*) is_expected_long_running=1 ;;
    *) is_expected_long_running=0 ;;
  esac
}

precmd() {
  local exit_code=$?
  local duration=$(( EPOCHSECONDS - ${cmd_start:-EPOCHSECONDS} ))

  # Ignore common interruption signals:
  # e.g. when stopping local dev server, don't need to notify
  # 130 = Ctrl+C (SIGINT)
  # 143 = SIGTERM
 case "$exit_code" in
  130|143) return 0 ;;
 esac

  # ignore expected long-running commands
  (( is_expected_long_running )) && return

  # Only notifies if delay is `n` seconds or longer; tweak value to taste
  (( duration > 1 )) || return

  # ignore on short-duration crashes
  (( exit_code == 0 &&  duration < 10)) || return

  # ignore spam
  (( EPOCHSECONDS - last_notify < 3 )) && return
  typeset -gi last_notify=$EPOCHSECONDS

  local message
  if (( exit_code == 0 )); then
    message="🟢 Completed in ${duration}s"
  else
    message="🔴 Failed (${exit_code}) after ${duration}s"
  fi

  $(command -v terminal-notifier) \
    -title "$cmd_title" \
    -message "$message" \
    -sound default
}

↗ External Resources