From 25f97f4ce55c2281e304f00b7d7e101150b2bcb4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:32:06 +0000 Subject: [PATCH] Implement on_quit event for global watchers Fixes #9682 --- docs/launch.rst | 8 ++++++++ kitty/boss.py | 28 +++++++++++++++++++++++++++- kitty/launch.py | 3 +++ kitty/window.py | 8 +++++++- 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/launch.rst b/docs/launch.rst index fd03e13d5..6a984f118 100644 --- a/docs/launch.rst +++ b/docs/launch.rst @@ -185,6 +185,14 @@ create :file:`~/.config/kitty/mywatcher.py` and use :option:`launch --watcher` = # managing all tabs in a single OS Window. ... + def on_quit(boss: Boss, window: Window, data: dict[str, Any]) -> None: + # called when kitty is about to quit. This is called in *global watchers* + # only. It is called twice: once before the quit confirmation dialog is + # shown (data['confirmed'] will be False) and once after the user has + # confirmed quitting (data['confirmed'] will be True). Setting + # data['aborted'] to True will abort the quit in both cases. + ... + Every callback is passed a reference to the global ``Boss`` object as well as the ``Window`` object the action is occurring on. The ``data`` object is a dict diff --git a/kitty/boss.py b/kitty/boss.py index 3f53e34e3..0a1e143c3 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -173,7 +173,7 @@ from .utils import ( timed_debug_print, which, ) -from .window import CommandOutput, CwdRequest, Window +from .window import CommandOutput, CwdRequest, Window, global_watchers if TYPE_CHECKING: @@ -2090,6 +2090,24 @@ class Boss: quit_confirmation_window_id: int = 0 + def _call_on_quit_watchers(self, data: dict[str, Any]) -> bool: + w = self.active_window + if w is None: + for window in self.window_id_map.values(): + w = window + break + if w is None: + return True + for watcher in global_watchers().on_quit: + try: + watcher(self, w, data) + except Exception: + import traceback + traceback.print_exc() + if data.get('aborted'): + return False + return True + @ac('win', 'Quit, closing all windows') def quit(self, *args: Any) -> None: windows = [] @@ -2102,6 +2120,8 @@ class Boss: num = num_active_windows if x < 0 else len(windows) needs_confirmation = x != 0 and num >= abs(x) if not needs_confirmation: + if not self._call_on_quit_watchers({'confirmed': True}): + return set_application_quit_request(IMPERATIVE_CLOSE_REQUESTED) return if current_application_quit_request() == CLOSE_BEING_CONFIRMED: @@ -2116,6 +2136,8 @@ class Boss: tab.set_active_window(w) return return + if not self._call_on_quit_watchers({'confirmed': False}): + return msg = msg or _('It has {} windows.').format(num) w = self.confirm(_('Are you sure you want to quit kitty?') + ' ' + msg, self.handle_quit_confirmation, window=active_window, title=_('Quit kitty?')) self.quit_confirmation_window_id = w.id @@ -2123,6 +2145,10 @@ class Boss: def handle_quit_confirmation(self, confirmed: bool) -> None: self.quit_confirmation_window_id = 0 + if confirmed: + if not self._call_on_quit_watchers({'confirmed': True}): + set_application_quit_request(NO_CLOSE_REQUESTED) + return set_application_quit_request(IMPERATIVE_CLOSE_REQUESTED if confirmed else NO_CLOSE_REQUESTED) def notify_on_os_window_death(self, address: str) -> None: diff --git a/kitty/launch.py b/kitty/launch.py index 119a0c2c4..75401348c 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -548,6 +548,9 @@ def load_watch_modules(watchers: Iterable[str]) -> Watchers | None: w = m.get('on_tab_bar_dirty') if callable(w): ans.on_tab_bar_dirty.append(w) + w = m.get('on_quit') + if callable(w): + ans.on_quit.append(w) return ans diff --git a/kitty/window.py b/kitty/window.py index 92a4f80cc..b32662855 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -305,6 +305,7 @@ class Watchers: on_cmd_startstop: list[Watcher] on_color_scheme_preference_change: list[Watcher] on_tab_bar_dirty: list[Watcher] + on_quit: list[Watcher] def __init__(self) -> None: self.on_resize = [] @@ -315,6 +316,7 @@ class Watchers: self.on_cmd_startstop = [] self.on_color_scheme_preference_change = [] self.on_tab_bar_dirty = [] + self.on_quit = [] def add(self, others: 'Watchers') -> None: def merge(base: list[Watcher], other: list[Watcher]) -> None: @@ -329,12 +331,14 @@ class Watchers: merge(self.on_cmd_startstop, others.on_cmd_startstop) merge(self.on_color_scheme_preference_change, others.on_color_scheme_preference_change) merge(self.on_tab_bar_dirty, others.on_tab_bar_dirty) + merge(self.on_quit, others.on_quit) def clear(self) -> None: del self.on_close[:], self.on_resize[:], self.on_focus_change[:] del self.on_set_user_var[:], self.on_title_change[:], self.on_cmd_startstop[:] del self.on_color_scheme_preference_change[:] del self.on_tab_bar_dirty[:] + del self.on_quit[:] def copy(self) -> 'Watchers': ans = Watchers() @@ -346,12 +350,14 @@ class Watchers: ans.on_cmd_startstop = self.on_cmd_startstop[:] ans.on_color_scheme_preference_change = self.on_color_scheme_preference_change[:] ans.on_tab_bar_dirty = self.on_tab_bar_dirty[:] + ans.on_quit = self.on_quit[:] return ans @property def has_watchers(self) -> bool: return bool(self.on_close or self.on_resize or self.on_focus_change or self.on_color_scheme_preference_change - or self.on_set_user_var or self.on_title_change or self.on_cmd_startstop or self.on_tab_bar_dirty) + or self.on_set_user_var or self.on_title_change or self.on_cmd_startstop or self.on_tab_bar_dirty + or self.on_quit) def call_watchers(windowref: Callable[[], Optional['Window']], which: str, data: dict[str, Any]) -> None: