From 601a6c9e3d263b701157d952ab46535cae053e0e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 20 Jun 2020 12:37:27 +0530 Subject: [PATCH] Allow sending arbitrary signals to the current foreground process in a window using either a mapping in kitty.conf or via remote control Fixes #2778 --- docs/changelog.rst | 3 +++ kitty/config.py | 14 +++++++++++ kitty/rc/signal_child.py | 53 ++++++++++++++++++++++++++++++++++++++++ kitty/window.py | 6 +++++ 4 files changed, 76 insertions(+) create mode 100644 kitty/rc/signal_child.py diff --git a/docs/changelog.rst b/docs/changelog.rst index 35ef9da19..a1a4598b4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,9 @@ To update |kitty|, :doc:`follow the instructions `. - Tall and Fat layouts: Add mappable actions to increase or decrease the number of full size windows (:iss:`2688`) +- Allow sending arbitrary signals to the current foreground process in a window + using either a mapping in kitty.conf or via remote control (:iss:`2778`) + - Allow sending the back and forward mouse buttons to terminal applications (:pull:`2742`) diff --git a/kitty/config.py b/kitty/config.py index dcbc004bc..a2783d7bf 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -140,6 +140,20 @@ def float_parse(func: str, rest: str) -> FuncArgsType: return func, (float(rest),) +@func_with_args('signal_child') +def signal_child_parse(func: str, rest: str) -> FuncArgsType: + import signal + signals = [] + for q in rest.split(): + try: + signum = getattr(signal, q.upper()) + except AttributeError: + log_error(f'Unknown signal: {rest} ignoring') + else: + signals.append(signum) + return func, tuple(signals) + + @func_with_args('change_font_size') def parse_change_font_size(func: str, rest: str) -> Tuple[str, Tuple[bool, Optional[str], float]]: vals = rest.strip().split(maxsplit=1) diff --git a/kitty/rc/signal_child.py b/kitty/rc/signal_child.py new file mode 100644 index 000000000..b19cf2719 --- /dev/null +++ b/kitty/rc/signal_child.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2020, Kovid Goyal + +from typing import TYPE_CHECKING, Optional + +from .base import ( + MATCH_WINDOW_OPTION, ArgsType, Boss, MatchError, PayloadGetType, + PayloadType, RCOptions, RemoteCommand, ResponseType, Window +) + +if TYPE_CHECKING: + from kitty.cli_stub import SignalChildRCOptions as CLIOptions + + +class SignalChild(RemoteCommand): + + ''' + signals: The signals, a list of names, such as SIGTERM, SIGKILL, SIGUSR1, etc. + match: Which windows to change the title in + ''' + + short_desc = 'Send a signal to the foreground process in the specified window' + desc = ( + 'Send one or more signals to the foreground process in the specified window(s).' + ' If you use the :option:`kitty @ signal-child --match` option' + ' the title will be set for all matched windows. By default, only the active' + ' window is affected. If you do not specify any signals, :code:`SIGINT` is sent by default.' + ' You can also map this to a keystroke in kitty.conf, for example::\n\n' + ' map F1 signal_child SIGTERM' + ) + options_spec = '''\ + ''' + '\n\n' + MATCH_WINDOW_OPTION + argspec = '[SIGNAL_NAME ...]' + + def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: + return {'match': opts.match, 'signals': [x.upper() for x in args] or ['SIGINT']} + + def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType: + import signal + windows = [window or boss.active_window] + match = payload_get('match') + if match: + windows = list(boss.match_windows(match)) + if not windows: + raise MatchError(match) + signals = tuple(getattr(signal, x) for x in payload_get('signals')) + for window in windows: + if window: + window.signal_child(*signals) + + +signal_child = SignalChild() diff --git a/kitty/window.py b/kitty/window.py index d8cf10287..d31aac90f 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -820,4 +820,10 @@ class Window: def scroll_to_mark(self, prev: bool = True, mark: int = 0) -> None: self.screen.scroll_to_next_mark(mark, prev) + + def signal_child(self, *signals: int) -> None: + pid = self.child.pid_for_cwd + if pid is not None: + for sig in signals: + os.kill(pid, sig) # }}}