Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for any macOS terminal #100

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 29 additions & 25 deletions applescript/functions
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,40 +27,31 @@ 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
if is-inside-tmux; then
is-current-tmux-pane-active
return $?
fi
else
return $?
else
return $?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is less understandable then it was before — this clause is part of the outer if, so it should either be the way it was before, or on the same level (4 spaces less indent) as the outer if.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, my bad. Fixed it

fi
}

Expand Down Expand Up @@ -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"
Expand Down
10 changes: 10 additions & 0 deletions applescript/resources/get-app-id-by-pid.applescript
Original file line number Diff line number Diff line change
@@ -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

40 changes: 20 additions & 20 deletions applescript/resources/is-apple-terminal-active.applescript
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions applescript/resources/is-window-active-by-pid.applescript
Original file line number Diff line number Diff line change
@@ -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

35 changes: 30 additions & 5 deletions lib
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function will infinitely recurse if ps doesn’t take the expected options (maybe on other BSDs? I dunno), or if there is no process with PID 1 (maybe in a container?).

I’m not especially worried about this, but it could be prevented by setting setopt localoptions errexit here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but it could be prevented by setting setopt localoptions errexit here

Tried to google this option and barely found anything. I think I fixed it much simpler :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works too!

setopt localoptions errexit might be hard to google because zsh ignores underscores in the options. Just running setopt gives a list of options, which are reported without underscores, but the manual lists them with underscores. So the above is equivalent to setopt local_options err_exit.

  • local_options causes the shell options to only be set for the duration of the local function.
  • err_exit causes the shell to exit when a command fails, like set -e. (It should probably have been err_return which causes the function to return.)

Docs:

# 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)
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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion notify.plugin.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions tests/apple-terminal.zunit
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
fi

osascript <<SCPT
tell app id "com.apple.terminal"
tell app id "com.apple.Terminal"
activate
end
SCPT
}

@test 'Apple Terminal: is-terminal-active - yes' {
osascript <<SCPT
tell app id "com.apple.terminal"
tell app id "com.apple.Terminal"
activate
end
SCPT
Expand All @@ -34,7 +34,7 @@ SCPT
@test 'Apple Terminal: is-terminal-active - app in background' {
osascript <<SCPT
tell app "System Events"
tell item 1 of (application processes whose bundle identifier is "com.apple.terminal")
tell item 1 of (application processes whose bundle identifier is "com.apple.Terminal")
set visible to false
end tell
end tell
Expand All @@ -47,7 +47,7 @@ SCPT
@test 'Apple Terminal: is-terminal-active - other tab active' {
osascript <<SCPT
tell app "System Events"
tell item 1 of (application processes whose bundle identifier is "com.apple.terminal")
tell item 1 of (application processes whose bundle identifier is "com.apple.Terminal")
set frontmost to true
end tell

Expand Down
51 changes: 51 additions & 0 deletions tests/macos-terminals.zunit
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env zunit

@setup {
load ../notify.plugin.zsh

if ! command -v osascript 1>/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 <<SCPT
tell app id "$app_id"
activate
end
SCPT
}

@test 'Any other macOS terminal: is-terminal-active - yes' {
osascript <<SCPT
tell app id "$app_id"
activate
end
SCPT

run is-terminal-active
assert $state equals 0
}

@test 'Any other macOS terminal: is-terminal-active - app in background' {
osascript <<SCPT
tell app "System Events"
tell item 1 of (application processes whose bundle identifier is "$app_id")
set visible to false
end tell
end tell
SCPT

run is-terminal-active
assert $state equals 1
}
44 changes: 42 additions & 2 deletions tests/zsh-notify.zunit
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

run zsh-notify success 3 <<<"notification text"
assert $state equals 0
assert "$(get-args)" same_as "-activate com.apple.terminal -title #win (in 3s)"
assert "$(get-args)" same_as "-activate com.apple.Terminal -title #win (in 3s)"
assert "$(get-stdin)" same_as "notification text"
}

Expand All @@ -29,7 +29,7 @@

run zsh-notify success 3 <<<"notification text"
assert $state equals 0
assert "$(get-args)" same_as "-activate com.apple.terminal -contentImage custom.gif -title #win (in 3s)"
assert "$(get-args)" same_as "-activate com.apple.Terminal -contentImage custom.gif -title #win (in 3s)"
assert "$(get-stdin)" same_as "notification text"
}

Expand Down Expand Up @@ -70,6 +70,46 @@
assert "$output" same_as "notification text"
}

@test 'zsh-notify - terminal-notifier in any other macOS terminal' {
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

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'
Expand Down