diff --git a/ntfy/cli.py b/ntfy/cli.py index f1dd725..74ae63b 100644 --- a/ntfy/cli.py +++ b/ntfy/cli.py @@ -21,22 +21,33 @@ from . import __version__, notify from .config import load_config, DEFAULT_CONFIG, OLD_DEFAULT_CONFIG from .data import scripts +try: + from .terminal import is_focused +except ImportError: + is_focused = lambda: True def run_cmd(args): if getattr(args, 'pid', False): return watch_pid(args) if not args.command: - stderr.write('usage: ntfy done [-h|-L N] command\n' - 'ntfy done: error: the following arguments ' - 'are required: command\n') - exit(1) - - start_time = time() - retcode = call(args.command) - duration = time() - start_time + if args.formatter: + args.command, retcode, duration = args.formatter + args.command, retcode, duration = ( + [args.command], int(retcode), int(duration)) + else: + stderr.write('usage: ntfy done [-h|-L N] command\n' + 'ntfy done: error: the following arguments ' + 'are required: command\n') + exit(1) + else: + start_time = time() + retcode = call(args.command) + duration = time() - start_time if args.longer_than is not None and duration <= args.longer_than: return + if args.unfocused_only and is_focused(): + return if emojize is not None and not args.no_emoji: prefix = ':white_check_mark: ' if retcode == 0 else ':x: ' else: @@ -68,8 +79,10 @@ def watch_pid(args): def auto_done(args): - if emojize is not None and not args.no_emoji: - print('export AUTO_NTFY_DONE_EMOJI=true') + if args.longer_than: + print('export AUTO_NTFY_DONE_LONGER_THAN=-L{}'.format(args.longer_than)) + if args.unfocused_only: + print('export AUTO_NTFY_DONE_UNFOCUSED_ONLY=-b') if args.shell == 'bash': print('source {}'.format(scripts['bash-preexec.sh'])) print('source {}'.format(scripts['auto-ntfy-done.sh'])) @@ -169,6 +182,19 @@ def __call__(self, parser, args, values, option_string=None): type=int, metavar='N', help="Only notify if the command runs longer than N seconds") +done_parser.add_argument( + '-b', + '--background-only', + action='store_true', + default=False, + dest='unfocused_only', + help="Only notify if shell isn't in the foreground") +done_parser.add_argument( + '--formatter', + metavar=('command', 'retcode', 'duration'), + nargs=3, + help="Format and send cmd, retcode & duration instead of running command. " + "Used internally by shell-integration") if psutil is not None: done_parser.add_argument( '-p', @@ -185,7 +211,20 @@ def __call__(self, parser, args, values, option_string=None): '--shell', default=path.split(environ.get('SHELL', ''))[1], choices=['bash', 'zsh'], - help='The shell to integrate ntfy with (default: your login shell)') + help='The shell to integrate ntfy with (default: $SHELL)') +shell_integration_parser.add_argument( + '-L', + '--longer-than', + type=int, + metavar='N', + help="Only notify if the command runs longer than N seconds") +shell_integration_parser.add_argument( + '-f', + '--foreground-too', + action='store_false', + default=True, + dest='unfocused_only', + help="Also notify if shell is in the foreground") shell_integration_parser.set_defaults(func=auto_done) diff --git a/ntfy/shell_integration/auto-ntfy-done.sh b/ntfy/shell_integration/auto-ntfy-done.sh index b8726c7..17e81a6 100644 --- a/ntfy/shell_integration/auto-ntfy-done.sh +++ b/ntfy/shell_integration/auto-ntfy-done.sh @@ -1,16 +1,16 @@ # In bash this requires https://github.com/rcaloras/bash-preexec # If sourcing this via ntfy auto-done, it is sourced for you. -# Default timeout is 10 seconds. -AUTO_NTFY_DONE_TIMEOUT=${AUTO_NTFY_DONE_TIMEOUT:-10} # Default to ignoring some well known interactive programs AUTO_NTFY_DONE_IGNORE=${AUTO_NTFY_DONE_IGNORE:-ntfy emacs info less mail man meld most mutt nano screen ssh sudo tail tmux vi vim} # Bash option example #AUTO_NTFY_DONE_OPTS='-b default' # Zsh option example #AUTO_NTFY_DONE_OPTS=(-b default) -# force emoji example -#AUTO_NTFY_DONE_EMOJI=true +# notify for unfocused only (Used by ntfy internally) +#AUTO_NTFY_DONE_UNFOCUSED_ONLY=-b +# notify for commands runing longer than N sec only (Used by ntfy internally) +#AUTO_NTFY_DONE_LONGER_THAN=-L10 function _ntfy_precmd () { [ -n "$ntfy_start_time" ] || return @@ -21,14 +21,9 @@ function _ntfy_precmd () { local appname=$(basename "${ntfy_command%% *}") [[ " $AUTO_NTFY_DONE_IGNORE " == *" $appname "* ]] && return - local human_duration=$(printf '%d:%02d\n' $(($duration/60)) $(($duration%60))) - local human_retcode - [ "$ret_value" -eq 0 ] && human_retcode='succeeded' || human_retcode='failed' - local prefix - if [[ "$AUTO_NTFY_DONE_EMOJI" == "true" ]]; then - [ "$ret_value" -eq 0 ] && prefix=':white_check_mark: ' || prefix=':x: ' - fi - ntfy $AUTO_NTFY_DONE_OPTS send "$prefix\"$ntfy_command\" $human_retcode in $human_duration minutes" + ntfy $AUTO_NTFY_DONE_OPTS done \ + $AUTO_NTFY_DONE_UNFOCUSED_ONLY $AUTO_NTFY_DONE_LONGER_THAN \ + --formatter "$ntfy_command" $ret_value $duration } function _ntfy_preexec () { diff --git a/ntfy/terminal.py b/ntfy/terminal.py new file mode 100644 index 0000000..0acc247 --- /dev/null +++ b/ntfy/terminal.py @@ -0,0 +1,65 @@ +from os import environ, ttyname +from subprocess import check_output, Popen, PIPE +from sys import platform, stdout + + +def get_tty(): + + window_id = int(check_output(['xprop', '-root', '\t$0', + '_NET_ACTIVE_WINDOW']).split()[1], 16) + return int(environ['WINDOWID']) == window_id + + +def linux_window_is_focused(): + window_id = int(check_output(['xprop', '-root', '\t$0', + '_NET_ACTIVE_WINDOW']).split()[1], 16) + return int(environ['WINDOWID']) == window_id + + +def osascript_tell(app, script): + p = Popen(['osascript'], stdin=PIPE, stdout=PIPE) + stdout, stderr = p.communicate( + 'tell application "{}"\n{}\nend tell'.format(app, script)) + return stdout.rstrip('\n') + + +def darwin_iterm2_shell_is_focused(): + focused_tty = osascript_tell( + 'iTerm', + 'tty of current session of current terminal', + ) + return focused_tty == ttyname(stdout.fileno()) + + +def darwin_terminal_shell_is_focused(): + focused_tty = osascript_tell( + 'Terminal', + 'tty of (first tab of (first window whose frontmost is true) ' + 'whose selected is true)', + ) + return focused_tty == ttyname(stdout.fileno()) + + +def darwin_app_shell_is_focused(): + current_appid = { + 'iTerm.app': 'iTerm', + 'Apple_Terminal': 'Terminal', + }.get(environ.get('TERM_PROGRAM')) + focused_appid = osascript_tell( + 'System Events', + 'name of first application process whose frontmost is true', + ) + if current_appid == focused_appid: + return { + 'Terminal': darwin_terminal_shell_is_focused, + 'iTerm': darwin_iterm2_shell_is_focused, + }[current_appid]() + + +def is_focused(): + if platform.startswith('linux'): + return linux_window_is_focused() + elif platform == 'darwin': + return darwin_app_shell_is_focused() + else: + return True diff --git a/tests/ntfy_test/cli.py b/tests/ntfy_test/cli.py index 82ab615..1ef9328 100644 --- a/tests/ntfy_test/cli.py +++ b/tests/ntfy_test/cli.py @@ -15,6 +15,7 @@ def test_default(self, mock_call): args.longer_than = -1 args.command = ['true'] args.pid = None + args.unfocused_only = False self.assertEqual('"true" succeeded in 0:00 minutes', run_cmd(args)) @patch('ntfy.cli.call') @@ -25,12 +26,14 @@ def test_emoji(self, mock_call): args.command = ['true'] args.pid = None args.no_emoji = False + args.unfocused_only = False self.assertEqual(':white_check_mark: "true" succeeded in 0:00 minutes', run_cmd(args)) def tests_usage(self): args = MagicMock() args.pid = False + args.formatter = False args.command = [] self.assertRaises(SystemExit, run_cmd, args) @@ -41,6 +44,7 @@ def test_longerthan(self, mock_call): args.longer_than = 1 args.command = ['true'] args.pid = None + args.unfocused_only = False self.assertEqual(None, run_cmd(args)) @@ -69,6 +73,7 @@ def test_watch_pid(self, mock_process): mock_process.return_value.cmdline.return_value = ['cmd'] args = MagicMock() args.pid = 1 + args.unfocused_only = False self.assertEqual('PID[1]: "cmd" finished in 0:00 minutes', run_cmd(args))