diff --git a/docs/changelog.rst b/docs/changelog.rst index dd6cf8fa7..8918396c0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -71,6 +71,9 @@ Detailed list of changes - ssh kitten: A new option :code:`--symlink-strategy` to control how symlinks are copied to the remote machine (:iss:`5249`) +- ssh kitten: Allow pressing Ctrl-C to abort ssh before the connection is + completed (:iss:`5271`) + - Bash integration: Fix declare not creating global variables in .bashrc (:iss:`5254`) diff --git a/kittens/ssh/main.py b/kittens/ssh/main.py index 976023af5..11f6b7d5c 100644 --- a/kittens/ssh/main.py +++ b/kittens/ssh/main.py @@ -39,7 +39,10 @@ from kitty.utils import ( set_echo as turn_off_echo ) -from ..tui.operations import restore_colors, save_colors +from ..tui.operations import ( + RESTORE_PRIVATE_MODE_VALUES, SAVE_PRIVATE_MODE_VALUES, Mode, + restore_colors, save_colors, set_mode +) from ..tui.utils import kitty_opts, running_in_tmux from .config import init_config from .copy import CopyInstruction @@ -547,10 +550,13 @@ def connection_sharing_args(kitty_pid: int) -> List[str]: def restore_terminal_state() -> Iterator[bool]: with open(os.ctermid()) as f: val = termios.tcgetattr(f.fileno()) + print(end=SAVE_PRIVATE_MODE_VALUES) + print(end=set_mode(Mode.HANDLE_TERMIOS_SIGNALS), flush=True) try: yield bool(val[3] & termios.ECHO) finally: termios.tcsetattr(f.fileno(), termios.TCSAFLUSH, val) + print(end=RESTORE_PRIVATE_MODE_VALUES, flush=True) def dcs_to_kitty(payload: Union[bytes, str], type: str = 'ssh') -> bytes: diff --git a/kittens/tui/operations.py b/kittens/tui/operations.py index 9b613dbac..f2f146df0 100644 --- a/kittens/tui/operations.py +++ b/kittens/tui/operations.py @@ -47,6 +47,7 @@ class Mode(Enum): ALTERNATE_SCREEN = 1049, '?' BRACKETED_PASTE = 2004, '?' PENDING_UPDATE = 2026, '?' + HANDLE_TERMIOS_SIGNALS = 19997, '?' def cmd(f: F) -> F: diff --git a/kitty/child.py b/kitty/child.py index 4911dd9e1..b377444da 100644 --- a/kitty/child.py +++ b/kitty/child.py @@ -448,3 +448,24 @@ class Child: with suppress(Exception): return environ_of_process(pid) return {} + + def send_signal_for_key(self, key_num: int) -> bool: + import signal + import termios + if self.child_fd is None: + return False + t = termios.tcgetattr(self.child_fd) + if not t[3] & termios.ISIG: + return False + cc = t[-1] + if key_num == cc[termios.VINTR]: + s = signal.SIGINT + elif key_num == cc[termios.VSUSP]: + s = signal.SIGTSTP + elif key_num == cc[termios.VQUIT]: + s = signal.SIGQUIT + else: + return False + pgrp = os.tcgetpgrp(self.child_fd) + os.killpg(pgrp, s) + return True diff --git a/kitty/keys.c b/kitty/keys.c index 965ee74f0..246076e03 100644 --- a/kitty/keys.c +++ b/kitty/keys.c @@ -218,6 +218,9 @@ on_key_input(GLFWkeyevent *ev) { schedule_write_to_child(w->id, 1, text, strlen(text)); debug("sent key as text to child\n"); } else if (size > 0) { + if (size == 1 && screen->modes.mHANDLE_TERMIOS_SIGNALS) { + if (screen_send_signal_for_key(screen, *encoded_key)) return; + } schedule_write_to_child(w->id, 1, encoded_key, size); debug("sent encoded key to child\n"); } else { diff --git a/kitty/modes.h b/kitty/modes.h index 5f0ef045d..aabbd9d38 100644 --- a/kitty/modes.h +++ b/kitty/modes.h @@ -84,3 +84,6 @@ // Pending updates mode #define PENDING_UPDATE (2026 << 5) + +// Handle Ctrl-C/Ctrl-Z mode +#define HANDLE_TERMIOS_SIGNALS (19997 << 5) diff --git a/kitty/screen.c b/kitty/screen.c index 296c22cde..85a544ac8 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -984,6 +984,7 @@ set_mode_from_const(Screen *self, unsigned int mode, bool val) { SIMPLE_MODE(DECARM) SIMPLE_MODE(BRACKETED_PASTE) SIMPLE_MODE(FOCUS_TRACKING) + SIMPLE_MODE(HANDLE_TERMIOS_SIGNALS) MOUSE_MODE(MOUSE_BUTTON_TRACKING, mouse_tracking_mode, BUTTON_MODE) MOUSE_MODE(MOUSE_MOTION_TRACKING, mouse_tracking_mode, MOTION_MODE) MOUSE_MODE(MOUSE_MOVE_TRACKING, mouse_tracking_mode, ANY_MODE) @@ -2085,6 +2086,20 @@ screen_handle_cmd(Screen *self, PyObject *cmd) { CALLBACK("handle_remote_cmd", "O", cmd); } +bool +screen_send_signal_for_key(Screen *self, char key) { + int ret = 0; + if (self->callbacks != Py_None) { + int cchar = key; + PyObject *callback_ret = PyObject_CallMethod(self->callbacks, "send_signal_for_key", "c", cchar); + if (callback_ret) { + ret = PyObject_IsTrue(callback_ret); + Py_DECREF(callback_ret); + } else { PyErr_Print(); } + } + return ret != 0; +} + void screen_push_colors(Screen *self, unsigned int idx) { if (colorprofile_push_colors(self->color_profile, idx)) self->color_profile->dirty = true; diff --git a/kitty/screen.h b/kitty/screen.h index 10ea23562..cc6c9d27e 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -14,7 +14,7 @@ typedef enum ScrollTypes { SCROLL_LINE = -999999, SCROLL_PAGE, SCROLL_FULL } Scr typedef struct { bool mLNM, mIRM, mDECTCEM, mDECSCNM, mDECOM, mDECAWM, mDECCOLM, mDECARM, mDECCKM, - mBRACKETED_PASTE, mFOCUS_TRACKING, mDECSACE; + mBRACKETED_PASTE, mFOCUS_TRACKING, mDECSACE, mHANDLE_TERMIOS_SIGNALS; MouseTrackingMode mouse_tracking_mode; MouseTrackingProtocol mouse_tracking_protocol; bool eight_bit_controls; // S8C1T @@ -263,6 +263,7 @@ void screen_report_key_encoding_flags(Screen *self); bool screen_detect_url(Screen *screen, unsigned int x, unsigned int y); int screen_cursor_at_a_shell_prompt(const Screen *); bool screen_fake_move_cursor_to_position(Screen *, index_type x, index_type y); +bool screen_send_signal_for_key(Screen *, char key); #define DECLARE_CH_SCREEN_HANDLER(name) void screen_##name(Screen *screen); DECLARE_CH_SCREEN_HANDLER(bell) DECLARE_CH_SCREEN_HANDLER(backspace) diff --git a/kitty/window.py b/kitty/window.py index 997c9dca8..9f2b5991c 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -857,6 +857,13 @@ class Window: '--ssh-connection-data', json.dumps(conn_data) ) + def send_signal_for_key(self, key_num: int) -> bool: + try: + return self.child.send_signal_for_key(key_num) + except OSError as err: + log_error(f'Failed to send signal for key to child with err: {err}') + return False + def focus_changed(self, focused: bool) -> None: if self.destroyed: return