diff --git a/applescript/functions b/applescript/functions index f963bb4..f498d42 100644 --- a/applescript/functions +++ b/applescript/functions @@ -1,5 +1,22 @@ # vim: set nowrap filetype=zsh: -# + +# run an AppleScript, selected from ./resources by basename given as the +# first argument, with all other arguments are positional arguments to the +# script's `on run` handler. +function run-applescript() { + local plugin_dir + + zstyle -s ':notify:' plugin-dir plugin_dir + source "$plugin_dir"/lib + + local script_name + + script_name="$1" + shift + + "$plugin_dir"/applescript/resources/"$script_name".applescript $@ 2>/dev/null +} + # is-terminal-active exits with status 0 when the current shell is running on an # active terminal window or tab, status 1 when the window or tab is in background # and status 2 if the current terminal is not supported (eg. it's not iTerm2 nor @@ -10,31 +27,22 @@ function is-terminal-active() { zstyle -s ':notify:' plugin-dir plugin_dir source "$plugin_dir"/lib - # run an AppleScript, selected from ./resources by basename given as the - # first argument, with all other arguments are positional arguments to the - # script's `on run` handler. - function run-applescript() { - local script_name - - script_name="$1" - shift - - "$plugin_dir"/applescript/resources/"$script_name".applescript $@ 2>/dev/null - } - # exit with code 0 if the terminal window/tab is active, code 1 if inactive. function is-terminal-window-active { - local term + local script_name script_arg if [[ "$TERM_PROGRAM" == 'iTerm.app' ]] || [[ -n "$ITERM_SESSION_ID" ]]; then - term=iterm2 + script_name=is-iterm2-active + script_arg=$(current-tty) elif [[ "$TERM_PROGRAM" == 'Apple_Terminal' ]] || [[ -n "$TERM_SESSION_ID" ]]; then - term=apple-terminal + script_name=is-apple-terminal-active + script_arg=$(current-tty) else - return 2 + script_name=is-window-active-by-pid + script_arg=$(top-level-ppid) fi - run-applescript is-"$term"-active "$(current-tty)" + run-applescript $script_name $script_arg } if is-terminal-window-active; then @@ -42,8 +50,8 @@ function is-terminal-active() { is-current-tmux-pane-active return $? fi - else - return $? + else + return $? fi } @@ -72,11 +80,7 @@ function zsh-notify() { title=$(notification-title "$type" time_elapsed "$time_elapsed") - if [[ "$TERM_PROGRAM" == 'iTerm.app' ]] || [[ -n "$ITERM_SESSION_ID" ]]; then - app_id="com.googlecode.iterm2" - elif [[ "$TERM_PROGRAM" == 'Apple_Terminal' ]] || [[ -n "$TERM_SESSION_ID" ]]; then - app_id="com.apple.terminal" - fi + app_id=$(run-applescript get-app-id-by-pid $(top-level-ppid)) if [[ -n "$app_id" ]]; then app_id_option="-activate $app_id" diff --git a/applescript/resources/get-app-id-by-pid.applescript b/applescript/resources/get-app-id-by-pid.applescript new file mode 100755 index 0000000..9c80403 --- /dev/null +++ b/applescript/resources/get-app-id-by-pid.applescript @@ -0,0 +1,10 @@ +#!/usr/bin/osascript + +on run argv + set targetPID to item 1 of argv + + tell application "System Events" + return bundle identifier of first process whose unix id is targetPID + end tell +end run + diff --git a/applescript/resources/is-apple-terminal-active.applescript b/applescript/resources/is-apple-terminal-active.applescript index 3c7f36d..1c2caab 100755 --- a/applescript/resources/is-apple-terminal-active.applescript +++ b/applescript/resources/is-apple-terminal-active.applescript @@ -1,24 +1,24 @@ #!/usr/bin/osascript -ss on run ttyName - try - set ttyName to first item of ttyName - on error - set ttyName to "" - end try - - if ttyName is equal to "" then error "Usage: is-apple-terminal-active.applescript TTY" - - tell application id "com.apple.terminal" - if frontmost is not true then error "Apple Terminal is not the frontmost application" - - -- fun stuff, with 2 tabs in one window AS reports 2 windows with one - -- tab each, and all the tabs are frontmost! - repeat with t in tabs of (windows whose frontmost is true) - if t's tty is equal to ttyName then return - end repeat - - error "Cannot find an active tab for '" & ttyName & "'" - - end tell + try + set ttyName to first item of ttyName + on error + set ttyName to "" + end try + + if ttyName is equal to "" then error "Usage: is-apple-terminal-active.applescript TTY" + + tell application id "com.apple.terminal" + if frontmost is not true then error "Apple Terminal is not the frontmost application" + + -- fun stuff, with 2 tabs in one window AS reports 2 windows with one + -- tab each, and all the tabs are frontmost! + repeat with t in tabs of (windows whose frontmost is true) + if t's tty is equal to ttyName then return + end repeat + + error "Cannot find an active tab for '" & ttyName & "'" + + end tell end run diff --git a/applescript/resources/is-window-active-by-pid.applescript b/applescript/resources/is-window-active-by-pid.applescript new file mode 100755 index 0000000..5e81e92 --- /dev/null +++ b/applescript/resources/is-window-active-by-pid.applescript @@ -0,0 +1,14 @@ +#!/usr/bin/osascript -ss + +on run argv + set targetPID to item 1 of argv + + tell application "System Events" + set appProcess to first process whose unix id is targetPID + end tell + + tell application (name of appProcess) + if frontmost is not true then error "Window with pid " & targetPID & " is not the frontmost application" + end tell +end run + diff --git a/lib b/lib index 19b0633..54f43bd 100644 --- a/lib +++ b/lib @@ -16,6 +16,31 @@ function current-tty { fi } +# Find the top level parent PID of current shell (not including root process), also accounting for TMUX +function top-level-ppid { + # Find parent PID of process by its PID + function ppid-of { + pid=$1 + ps -p $pid -o ppid= + } + + pid=$$ + + if is-inside-tmux; then + pid=$(tmux display-message -p '#{client_pid}') + fi + + ppid=$(ppid-of $pid) + + # We're assuming the root process PID is equal to 1 + while [[ $ppid -ne 1 ]]; do + pid=$ppid + ppid=$(ppid-of $pid) || return + done + + echo $pid +} + # Exit with 0 if given TMUX pane is the active one. function is-current-tmux-pane-active { is-inside-tmux || return 1 @@ -40,11 +65,11 @@ function notification-title { zstyle -s ':notify:' "$type"-title title while [[ $# -gt 0 ]]; do - k="$1" - v="$2" - title=$(echo $title | sed "s/#{$k}/$v/") - shift - shift + k="$1" + v="$2" + title=$(echo $title | sed "s/#{$k}/$v/") + shift + shift done echo $title diff --git a/notify.plugin.zsh b/notify.plugin.zsh index 609f919..a7e8fcd 100644 --- a/notify.plugin.zsh +++ b/notify.plugin.zsh @@ -2,7 +2,7 @@ plugin_dir="$(dirname $0:A)" -if [[ "$TERM_PROGRAM" == 'iTerm.app' ]] || [[ "$TERM_PROGRAM" == 'Apple_Terminal' ]] || [[ -n "$ITERM_SESSION_ID" ]] || [[ -n "$TERM_SESSION_ID" ]]; then +if command -v osascript >/dev/null 2>&1; then source "$plugin_dir"/applescript/functions elif [[ "$DISPLAY" != '' ]] && command -v xdotool > /dev/null 2>&1 && command -v wmctrl > /dev/null 2>&1; then source "$plugin_dir"/xdotool/functions diff --git a/tests/apple-terminal.zunit b/tests/apple-terminal.zunit index 0731eee..6ed59ab 100644 --- a/tests/apple-terminal.zunit +++ b/tests/apple-terminal.zunit @@ -14,7 +14,7 @@ fi osascript </dev/null 2>/dev/null || [[ "$TERM_PROGRAM" == "Apple_Terminal" ]] \ + || [[ "$TERM_PROGRAM" == "iTerm.app" ]] || [[ -n "$ITERM_SESSION_ID" ]]; + then + skip 'this test must be run in macOS terminal that is not iTerm2 or Apple Terminal (e.g. Alacritty)' + fi + + app_id=$(osascript -e 'tell application "System Events" to return bundle identifier of first process where it is frontmost') +} + +@teardown { + if ! command -v osascript 1>/dev/null 2>/dev/null || [[ "$TERM_PROGRAM" == "Apple_Terminal" ]] \ + || [[ "$TERM_PROGRAM" == "iTerm.app" ]] || [[ -n "$ITERM_SESSION_ID" ]]; + then + return + fi + + osascript </dev/null 2>/dev/null || [[ "$TERM_PROGRAM" == "Apple_Terminal" ]] \ + || [[ "$TERM_PROGRAM" == "iTerm.app" ]] || [[ -n "$ITERM_SESSION_ID" ]]; + then + skip 'must be run in macOS terminal that is not iTerm2 or Apple Terminal (e.g. Alacritty)' + fi + + run zsh-notify success 3 <<<"notification text" + assert $state equals 0 + + run get-args + assert $state equals 0 + assert "$output" matches "-activate [^\\s]* -title \#win \\(in 3s\\)" + + run get-stdin + assert $state equals 0 + assert "$output" same_as "notification text" +} + +@test 'zsh-notify - terminal-notifier in any other macOS terminal with icon' { + if ! command -v osascript 1>/dev/null 2>/dev/null || [[ "$TERM_PROGRAM" == "Apple_Terminal" ]] \ + || [[ "$TERM_PROGRAM" == "iTerm.app" ]] || [[ -n "$ITERM_SESSION_ID" ]]; + then + skip 'must be run in macOS terminal that is not iTerm2 or Apple Terminal (e.g. Alacritty)' + fi + + zstyle ':notify:*' success-icon custom.gif + + run zsh-notify success 3 <<<"notification text" + assert $state equals 0 + + run get-args + assert $state equals 0 + assert "$output" matches "-activate [^\\s]* -contentImage custom.gif -title #win \\(in 3s\\)" + + run get-stdin + assert $state equals 0 + assert "$output" same_as "notification text" +} + @test 'zsh-notify - notify-send' { if ! command -v xdotool 1>/dev/null 2>/dev/null; then skip 'must be run on linux'