commit d8dff7b0c34617744f46c7969a9763d464e44972 Author: Bruno BELANYI Date: Sun Aug 13 14:57:41 2023 +0100 done: initial version This is basically a translation straight from the fish plug-in, further enhanced to make it more suitable for zsh idioms. diff --git a/done.plugin.zsh b/done.plugin.zsh new file mode 100644 index 0000000..142aaaf --- /dev/null +++ b/done.plugin.zsh @@ -0,0 +1,236 @@ +# I don't care about masking `local` return value +# shellcheck disable=2155 + +# Exit early if non-interactive +[[ -o interactive ]] || return + +if [ -z "$SSH_CLIENT" ]; then + : # Keep executing if we're graphical +elif [ "${DONE_ALLOW_NONGRAPHICAL:-0}" -ne 0 ] && (( ${+functions[done_send_notification]} )); then + : # Or if the user really wants us to +else + # Exit early if otherwise + return +fi + +: "${DONE_MIN_CMD_DURATION=5}" +: "${DONE_EXCLUDE=}" +: "${DONE_NOTIFY_SOUND=0}" +: "${DONE_NOTIFICATION_URGENCY_LEVEL=normal}" +: "${DONE_NOTIFICATION_URGENCY_LEVEL_FAILURE=critical}" +: "${DONE_SWAY_IGNORE_VISIBLE=0}" +# functions: done_format_title, done_format_message, done_send_notification + +# EPOCHSECONDS is faster than using `date` +zmodload zsh/datetime +# Necessary to add the hooks +autoload -U add-zsh-hook + +__done_get_focused_window_id() { + if (( ${+commands[lsappinfo]} )); then + lsappinfo info -only bundleID "$(lsappinfo front)" | cut -d '"' -f4 + elif [ -n "$SWAYSOCK" ] && (( ${+commands[jq]} )); then + swaymsg --type get_tree | jq '.. | objects | select(.focused == true) | .id' + elif [ "$XDG_SESSION_DESKTOP" = gnome ] && (( ${+commands[gdbus]} )); then + gdbus call \ + --session \ + --dest org.gnome.Shell \ + --object-path /org/gnome/Shell \ + --method org.gnome.Shell.Eval 'global.display.focus_window.get_id()' + elif (( ${+commands[xprop]} )) && [ -n "$DISPLAY" ] && xprop -grammar &>/dev/null; then + xprop -root 32x '\t$0' _NET_ACTIVE_WINDOW | cut -f 2 + fi +} + +__done_is_tmux_window_active() { + local pid="$$" + local tmux_pid + + while true; do + tmux_pid="$(ps -o ppid= -p "$pid")" + tmux_pid="$((tmux_pid))" # Trick to get rid of whitespace from `ps` output + # Stop once `tmux_pid` is actually tmux + case "$(basename "$(ps -o command= -p "$tmux_pid")")" in + tmux*) break ;; + esac + pid="$tmux_pid" + done + + # Window is considered active only if the session is attached + tmux list-panes -a -F "#{session_attached} #{window_active} #{pane_pid}" | + grep -q "1 1 $pid" +} +__done_is_screen_window_active() { + screen -ls | grep -q -E "$STY\s+\(Attached" +} + +__done_is_process_window_focused() { + # Send notification for every command in non-graphical environment + if [ "$DONE_ALLOW_NONGRAPHICAL" -ne 0 ]; then + return 1 + fi + + local current_window_id="$(__done_get_focused_window_id)" + if [ "$DONE_SWAY_IGNORE_VISIBLE" -ne 0 ] && + [ -n "$SWAYSOCK" ] && + (( ${+commands[jq]} )); then + local is_visible="$(swaymsg -t get_tree | jq ".. | objects | select(.id == $__done_initial_window_id) | .visible")" + [ "$is_visible" = "true" ] + return $? + elif [ "$current_window_id" != "$__done_initial_window_id" ]; then + return 1 + fi + + if (( ${+commands[tmux]} )) && [ -n "$TMUX" ]; then + __done_is_tmux_window_active + return $? + fi + if (( ${+commands[screen]} )) && [ -n "$STY" ]; then + __done_is_screen_window_active + return $? + fi + + return 0 +} + +__done_humanize_duration() { + local seconds=$(($1 % 60)) + local minutes=$(($1 / 60)) + local hours=$(($1 / 60 / 60)) + + if [ "$hours" -gt 0 ]; then + printf '%sh ' "$hours" + fi + if [ "$minutes" -gt 0 ]; then + printf '%sm ' "$minutes" + fi + if [ "$seconds" -gt 0 ]; then + printf '%ss' "$seconds" + fi +} + +__done_do_bell() { + if [ "$DONE_NOTIFY_SOUND" -ne 0 ]; then + printf "\a" + fi +} + +__done_is_ignored_command() { + if [ -z "$DONE_EXCLUDE" ]; then + return 1 + fi + # shellcheck disable=2154 + printf '%s' "$__done_last_command" | grep -q -v -P "$DONE_EXCLUDE" +} + +__done_notify() { + local exit_status="$1" + local title="$2" + local message="$3" + + if (( ${+functions[done_send_notification]} )); then + done_send_notification "$exit_status" "$title" "$message" + __done_do_bell + elif (( ${+commands[terminal-notifier]} )); then + local sound=() + + if [ "$DONE_NOTIFY_SOUND" -ne 0 ]; then + sound=(-sound default) + fi + + terminal-notifier \ + -message "$message" \ + -title "$title" \ + -sender "$__done_initial_window_id" \ + "${sound[@]}" + elif (( ${+commands[osascript]} )); then + osascript -e "display notification \"$message\" with title \"$title\"" + __done_do_bell + elif (( ${+commands[notify-send]} )); then + local urgency="${DONE_NOTIFICATION_URGENCY_LEVEL}" + + if [ "$exit_status" -ne 0 ]; then + urgency="${DONE_NOTIFICATION_URGENCY_LEVEL_FAILURE}" + fi + + notify-send \ + --hint=int:transient:1 \ + --urgency="$urgency" \ + --icon=utilities-terminal \ + --app-name=zsh \ + "$title" "$message" + __done_do_bell + elif (( ${+commands[notify-desktop]} )); then + local urgency="${DONE_NOTIFICATION_URGENCY_LEVEL}" + + if [ "$exit_status" -ne 0 ]; then + urgency="${DONE_NOTIFICATION_URGENCY_LEVEL_FAILURE}" + fi + + notify-desktop \ + --urgency="$urgency" \ + --icon=utilities-terminal \ + --app-name=zsh \ + "$title" "$message" + __done_do_bell + else + # Fallback to bell when nothing else is available + printf "\a" + fi +} + +__done_format_title() { + local exit_status="$1" + local cmd_duration="$2" + local last_command="$3" + + if (( ${+functions[done_format_title]} )); then + done_format_title "$exit_status" "$cmd_duration" "$last_command" + else + local humanized_duration="$(__done_humanize_duration "$cmd_duration")" + local title="Done in $humanized_duration" + if [ "$exit_status" -ne 0 ]; then + title="Failed ($exit_status) after $humanized_duration" + fi + printf '%s' "$title" + fi +} + +__done_format_message() { + local exit_status="$1" + local cmd_duration="$2" + local last_command="$3" + + if (( ${+functions[done_format_message]} )); then + done_format_message "$exit_status" "$cmd_duration" "$last_command" + else + local wd="${PWD/$HOME/~}" + local message="$wd/ $last_command" + printf '%s' "$message" + fi +} + +__done_started() { + __done_initial_window_id="$(__done_get_focused_window_id)" + __done_timestamp="$EPOCHSECONDS" + __done_last_command="${1:-$2}" +} +add-zsh-hook preexec __done_started + +__done_ended() { + : "${__done_timestamp:=$EPOCHSECONDS}" # fix the value on first source + local exit_status="$?" + local cmd_duration=$((EPOCHSECONDS - __done_timestamp)) + + if [ "$cmd_duration" -gt "$DONE_MIN_CMD_DURATION" ] && + ! __done_is_process_window_focused && + ! __done_is_ignored_command; then + local format_args=("$exit_status" "$cmd_duration" "$__done_last_command") + + local title="$(__done_format_title "${format_args[@]}")" + local message="$(__done_format_message "${format_args[@]}")" + + __done_notify "$exit_status" "$title" "$message" + fi +} +add-zsh-hook precmd __done_ended