Implement on_quit event for global watchers

Fixes #9682
This commit is contained in:
copilot-swe-agent[bot] 2026-03-17 02:32:06 +00:00 committed by Kovid Goyal
parent 02a34ecd04
commit 25f97f4ce5
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
4 changed files with 45 additions and 2 deletions

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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: