diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 160df9010..aed8f696e 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1210,6 +1210,7 @@ class Screen: def current_pointer_shape(self) -> str: ... def change_pointer_shape(self, op: str, name: str) -> None: ... + def bell(self) -> None: ... def set_tab_bar_render_data( os_window_id: int, screen: Screen, left: int, top: int, right: int, bottom: int diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 32d051a70..c6e936f5d 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -3168,11 +3168,35 @@ :code:`always` Always send a notification, regardless of window state. -Furthermore, you can set the minimum duration for what is considered a +Furthermore, you can set two optional arguments: + +1. The minimum duration for what is considered a long running command. The default is 5 seconds. Specify a second argument to set the duration. For example: :code:`invisible 15`. Do not set the value too small, otherwise a command that launches a new OS Window and exits will spam a notification. + +2. The action to perform. The default is :code:`notify`. The possible values are: + +:code:`notify` + Send a desktop notification. + +:code:`bell` + Ring the terminal bell. + +:code:`command` + Run a custom command. All subsequent arguments are the cmdline to run. + +Some more examples:: + + # Send a notification when a command takes more than 5 seconds in an unfocused window + notify_on_cmd_finish unfocused + # Send a notification when a command takes more than 10 seconds in a invisible window + notify_on_cmd_finish invisible 10.0 + # Ring a bell when a command takes more than 10 seconds in a invisible window + notify_on_cmd_finish invisible 10.0 bell + # Run 'notify-send' when a command takes more than 10 seconds in a invisible window + notify_on_cmd_finish invisible 10.0 command notify-send command finished ''' ) diff --git a/kitty/options/types.py b/kitty/options/types.py index 7ba4c34b3..a494fe327 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -8,7 +8,10 @@ from kitty.fast_data_types import Color, SingleKey import kitty.fast_data_types import kitty.fonts -from kitty.options.utils import AliasMap, KeyDefinition, KeyMap, MouseMap, MouseMapping, SequenceMap, TabBarMarginHeight +from kitty.options.utils import ( + AliasMap, KeyDefinition, KeyMap, MouseMap, MouseMapping, NotifyOnCmdFinish, SequenceMap, + TabBarMarginHeight +) import kitty.options.utils from kitty.types import FloatEdges import kitty.types @@ -546,7 +549,7 @@ class Options: mark3_background: Color = Color(242, 116, 188) mark3_foreground: Color = Color(0, 0, 0) mouse_hide_wait: float = 0.0 if is_macos else 3.0 - notify_on_cmd_finish: typing.Tuple[str, float] = ('never', 5.0) + notify_on_cmd_finish: NotifyOnCmdFinish = NotifyOnCmdFinish(when='never', duration=5.0, action='notify', cmdline=[]) open_url_with: typing.List[str] = ['default'] paste_actions: typing.FrozenSet[str] = frozenset({'confirm', 'quote-urls-at-prompt'}) placement_strategy: choices_for_placement_strategy = 'center' diff --git a/kitty/options/utils.py b/kitty/options/utils.py index b816125ef..4245ea75d 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -702,14 +702,32 @@ def active_tab_title_template(x: str) -> Optional[str]: return None if x == 'none' else x -def notify_on_cmd_finish(x: str) -> Tuple[str, float]: +class NotifyOnCmdFinish(NamedTuple): + when: str + duration: float + action: str + cmdline: List[str] + +def notify_on_cmd_finish(x: str) -> NotifyOnCmdFinish: parts = x.split() if parts[0] not in ('never', 'unfocused', 'invisible', 'always'): - raise ValueError(f'Unknown notify_on_cmd_finish value: {x}') + raise ValueError(f'Unknown notify_on_cmd_finish value: {parts[0]}') + when = parts[0] duration = 5.0 if len(parts) > 1: duration = float(parts[1]) - return parts[0], duration + action = 'notify' + cmdline = [] + if len(parts) > 2: + if parts[2] not in ('notify', 'bell', 'command'): + raise ValueError(f'Unknown notify_on_cmd_finish action: {parts[2]}') + action = parts[2] + if action == 'command': + if len(parts) > 3: + cmdline = parts[3:] + else: + raise ValueError('notify_on_cmd_finish `command` action needs a command line') + return NotifyOnCmdFinish(when, duration, action, cmdline) def config_or_absolute_path(x: str, env: Optional[Dict[str, str]] = None) -> Optional[str]: diff --git a/kitty/screen.c b/kitty/screen.c index baf78ca7e..759e598c9 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -4519,6 +4519,7 @@ line_edge_colors(Screen *self, PyObject *a UNUSED) { } WRAP0(update_only_line_graphics_data) +WRAP0(bell) #define MND(name, args) {#name, (PyCFunction)name, args, #name}, @@ -4609,6 +4610,7 @@ static PyMethodDef methods[] = { MND(marked_cells, METH_NOARGS) MND(scroll_to_next_mark, METH_VARARGS) MND(update_only_line_graphics_data, METH_NOARGS) + MND(bell, METH_NOARGS) {"select_graphic_rendition", (PyCFunction)_select_graphic_rendition, METH_VARARGS, ""}, {NULL} /* Sentinel */ diff --git a/kitty/window.py b/kitty/window.py index 5669253b8..fe1a83744 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -1339,15 +1339,26 @@ def cmd_output_marking(self, is_start: int) -> None: self.call_watchers(self.watchers.on_cmd_startstop, {"is_start": False, "time": end_time}) opts = get_options() - mode, duration = opts.notify_on_cmd_finish + when, duration, action, cmdline = opts.notify_on_cmd_finish - if last_cmd_output_duration >= duration and mode != 'never': + if last_cmd_output_duration >= duration and when != 'never': cmd = NotificationCommand() cmd.title = 'kitty' cmd.body = 'Command finished in a background window.\nClick to focus.' cmd.actions = 'focus' - cmd.only_when = OnlyWhen(mode) - notify_with_command(cmd, self.id) + cmd.only_when = OnlyWhen(when) + if action == 'notify': + notify_with_command(cmd, self.id) + elif action == 'bell': + def bell(title: str, body: str, identifier: str) -> None: + self.screen.bell() + notify_with_command(cmd, self.id, notify_implementation=bell) + elif action == 'command': + def run_command(title: str, body: str, identifier: str) -> None: + open_cmd(cmdline) + notify_with_command(cmd, self.id, notify_implementation=run_command) + else: + raise ValueError(f'Unknown action in option `notify_on_cmd_finish`: {action}') # }}} # mouse actions {{{