Consider windows with background processes as active for confirm_close

Fixes #8358
This commit is contained in:
Kovid Goyal
2025-02-21 14:26:12 +05:30
parent f585175f0a
commit ba31763acf
5 changed files with 97 additions and 99 deletions

View File

@@ -120,6 +120,8 @@ Detailed list of changes
- Speed up rendering of box drawing characters by moving the implementation to native code - Speed up rendering of box drawing characters by moving the implementation to native code
- When confirming if a window should be closed consider it active if it has running background processes (:iss:`8358`)
- Remote control: `kitten @ scroll-window`: Allow scrolling to previous/next prompt - Remote control: `kitten @ scroll-window`: Allow scrolling to previous/next prompt
- macOS: Fix fallback font rendering for bold/italic text not working for some symbols that are present in the Menlo regular face but not the bold/italic faces (:iss:`8282`) - macOS: Fix fallback font rendering for bold/italic text not working for some symbols that are present in the Menlo regular face but not the bold/italic faces (:iss:`8282`)

View File

@@ -959,6 +959,30 @@ class Boss:
def close_window(self) -> None: def close_window(self) -> None:
self.mark_window_for_close(self.window_for_dispatch) self.mark_window_for_close(self.window_for_dispatch)
def close_windows_with_confirmation_msg(self, windows: Iterable[Window], active_window: Window | None) -> tuple[str, int]:
num_running_programs = 0
num_background_programs = 0
running_program = background_program = ''
windows = sorted(windows, key=lambda w: 0 if w is active_window else 1)
for window in windows:
if window.has_running_program:
num_running_programs += 1
running_program = running_program or (window.child.foreground_cmdline or [''])[0]
elif bp := window.child.background_processes:
num_background_programs += len(bp)
for q in bp:
background_program = background_program or (q['cmdline'] or [''])[0]
if num := num_running_programs + num_background_programs:
if num_running_programs:
return ngettext(_('It is running: {}.'), _('It is running: {} and {} other programs.'), num_running_programs).format(
green(running_program), num_running_programs - 1), num
if num_background_programs:
return ngettext(_('It is running in the background: {}.'), _('It is running in the background: {} and {} other programs.'),
num_background_programs).format(green(background_program), num_background_programs - 1) + ' ' + _(
'\n\nBackground programs should be run with the disown command'
' to allow them to continue running when the terminal is closed.'), num
return '', 0
@ac('win', ''' @ac('win', '''
Close window with confirmation Close window with confirmation
@@ -972,12 +996,11 @@ class Boss:
window = self.window_for_dispatch or self.active_window window = self.window_for_dispatch or self.active_window
if window is None: if window is None:
return return
if not ignore_shell or window.has_running_program: msg = self.close_windows_with_confirmation_msg((window,), window)[0]
msg = _('Are you sure you want to close this window?') if not msg and not ignore_shell:
if window.has_running_program: msg = _('It is running a shell.')
msg += ' ' + _('It is running: {}').format((window.child.foreground_cmdline or [''])[0]) if msg:
else: msg = _('Are you sure you want to close this window?') + ' ' + msg
msg += ' ' + _('It is running a shell')
self.confirm(msg, self.handle_close_window_confirmation, window.id, window=window, title=_('Close window?')) self.confirm(msg, self.handle_close_window_confirmation, window.id, window=window, title=_('Close window?'))
else: else:
self.mark_window_for_close(window) self.mark_window_for_close(window)
@@ -1115,12 +1138,14 @@ class Boss:
) )
def confirm_tab_close(self, tab: Tab) -> None: def confirm_tab_close(self, tab: Tab) -> None:
msg, num_active_windows = self.close_windows_with_confirmation_msg(tab, tab.active_window)
x = get_options().confirm_os_window_close x = get_options().confirm_os_window_close
num = tab.number_of_windows_with_running_programs if x < 0 else len(tab) num = num_active_windows if x < 0 else len(tab)
needs_confirmation = x != 0 and num >= abs(x) needs_confirmation = x != 0 and num >= abs(x)
if not needs_confirmation: if not needs_confirmation:
self.close_tab_no_confirm(tab) self.close_tab_no_confirm(tab)
return return
msg = msg or _('It has {} windows?').format(num)
if tab is not self.active_tab: if tab is not self.active_tab:
tm = tab.tab_manager_ref() tm = tab.tab_manager_ref()
if tm is not None: if tm is not None:
@@ -1130,22 +1155,7 @@ class Boss:
if w in tab: if w in tab:
tab.set_active_window(w) tab.set_active_window(w)
return return
program = active_program = '' msg = _('Are you sure you want to close this tab?') + ' ' + msg
active_window = tab.active_window
num = -1
for w in tab:
if w.has_running_program:
program = os.path.basename((w.child.foreground_cmdline or ('',))[0])
num += 1
if w is active_window:
active_program = program
if num > 0:
msg = ngettext(
'Are you sure you want to close this tab? It is running the {} program and one other program.',
'Are you sure you want to close this tab? It is running the {} program and {} other programs.', num)
else:
msg = _('Are you sure you want to close this tab? It is running the {} program')
msg = msg.format(green(active_program or program or 'shell'), num)
w = self.confirm(msg, self.handle_close_tab_confirmation, tab.id, window=tab.active_window, title=_('Close tab?')) w = self.confirm(msg, self.handle_close_tab_confirmation, tab.id, window=tab.active_window, title=_('Close tab?'))
tab.confirm_close_window_id = w.id tab.confirm_close_window_id = w.id
@@ -1757,38 +1767,22 @@ class Boss:
def confirm_os_window_close(self, os_window_id: int) -> None: def confirm_os_window_close(self, os_window_id: int) -> None:
tm = self.os_window_map.get(os_window_id) tm = self.os_window_map.get(os_window_id)
if tm is None:
self.mark_os_window_for_close(os_window_id)
return
active_window = tm.active_window
windows = []
for tab in tm:
windows += list(tab)
msg, num_active_windows = self.close_windows_with_confirmation_msg(windows, active_window)
q = get_options().confirm_os_window_close q = get_options().confirm_os_window_close
num = 0 if tm is None else (tm.number_of_windows_with_running_programs if q < 0 else tm.number_of_windows) num = num_active_windows if q < 0 else len(windows)
needs_confirmation = tm is not None and q != 0 and num >= abs(q) needs_confirmation = tm is not None and q != 0 and num >= abs(q)
if not needs_confirmation: if not needs_confirmation:
self.mark_os_window_for_close(os_window_id) self.mark_os_window_for_close(os_window_id)
return return
if tm is None: msg = msg or _('It has {} windows?').format(num)
return msg = _('Are you sure you want to close this OS Window?') + ' ' + msg
if tm.confirm_close_window_id and tm.confirm_close_window_id in self.window_id_map:
cw = self.window_id_map[tm.confirm_close_window_id]
ctab = cw.tabref()
if ctab is not None and ctab in tm and cw in ctab:
tm.set_active_tab(ctab)
ctab.set_active_window(cw)
return
program = active_program = ''
active_window = tm.active_window
num = -1
for tab in tm:
for w in tab:
if w.has_running_program:
num += 1
program = os.path.basename((w.child.foreground_cmdline or ('',))[0])
if w is active_window:
active_program = program
if num > 0:
msg = ngettext(
'Are you sure you want to close this OS window? It is running the {} program and one other program.',
'Are you sure you want to close this OS window? It is running the {} program and {} other programs.', num)
else:
msg = _('Are you sure you want to close this OS window? It is running the {} program')
msg = msg.format(green(active_program or program or 'shell'), num)
w = self.confirm(msg, self.handle_close_os_window_confirmation, os_window_id, window=tm.active_window, title=_('Close OS window')) w = self.confirm(msg, self.handle_close_os_window_confirmation, os_window_id, window=tm.active_window, title=_('Close OS window'))
tm.confirm_close_window_id = w.id tm.confirm_close_window_id = w.id
@@ -1818,12 +1812,15 @@ class Boss:
@ac('win', 'Quit, closing all windows') @ac('win', 'Quit, closing all windows')
def quit(self, *args: Any) -> None: def quit(self, *args: Any) -> None:
tm = self.active_tab windows = []
num = 0
x = get_options().confirm_os_window_close
for q in self.os_window_map.values(): for q in self.os_window_map.values():
num += q.number_of_windows_with_running_programs if x < 0 else q.number_of_windows for qt in q:
needs_confirmation = tm is not None and x != 0 and num >= abs(x) windows += list(qt)
active_window = self.active_window
msg, num_active_windows = self.close_windows_with_confirmation_msg(windows, active_window)
x = get_options().confirm_os_window_close
num = num_active_windows if x < 0 else len(windows)
needs_confirmation = x != 0 and num >= abs(x)
if not needs_confirmation: if not needs_confirmation:
set_application_quit_request(IMPERATIVE_CLOSE_REQUESTED) set_application_quit_request(IMPERATIVE_CLOSE_REQUESTED)
return return
@@ -1839,24 +1836,8 @@ class Boss:
tab.set_active_window(w) tab.set_active_window(w)
return return
return return
assert tm is not None msg = msg or _('It has {} windows.').format(num)
program = active_program = '' w = self.confirm(_('Are you sure you want to quit kitty?') + ' ' + msg, self.handle_quit_confirmation, window=active_window, title=_('Quit kitty?'))
active_window = self.active_window
num = -1
for w in self.all_windows:
if w.has_running_program:
program = os.path.basename((w.child.foreground_cmdline or ('',))[0])
num += 1
if w is active_window:
active_program = program
if num > 0:
msg = ngettext(
'Are you sure you want to quit kitty? It is running the {} program and one other program.',
'Are you sure you want to quit kitty? It is running the {} program and {} other programs.', num)
else:
msg = _('Are you sure you want to quit kitty? It is running the {} program')
msg = msg.format(green(active_program or program or 'shell'), num)
w = self.confirm(msg, self.handle_quit_confirmation, window=tm.active_window, title=_('Quit kitty?'))
self.quit_confirmation_window_id = w.id self.quit_confirmation_window_id = w.id
set_application_quit_request(CLOSE_BEING_CONFIRMED) set_application_quit_request(CLOSE_BEING_CONFIRMED)

View File

@@ -7,7 +7,7 @@ from collections import defaultdict
from collections.abc import Generator, Sequence from collections.abc import Generator, Sequence
from contextlib import contextmanager, suppress from contextlib import contextmanager, suppress
from itertools import count from itertools import count
from typing import TYPE_CHECKING, DefaultDict, Optional, TypedDict from typing import TYPE_CHECKING, DefaultDict, Iterable, Optional, TypedDict
import kitty.fast_data_types as fast_data_types import kitty.fast_data_types as fast_data_types
@@ -111,6 +111,13 @@ def cached_process_data() -> Generator[None, None, None]:
delattr(process_group_map, 'cached_map') delattr(process_group_map, 'cached_map')
def session_id(pids: Iterable[int]) -> int:
for pid in pids:
with suppress(OSError):
if (sid := os.getsid(pid)) > -1:
return sid
return -1
def parse_environ_block(data: str) -> dict[str, str]: def parse_environ_block(data: str) -> dict[str, str]:
"""Parse a C environ block of environment variables into a dictionary.""" """Parse a C environ block of environment variables into a dictionary."""
# The block is usually raw data from the target process. It might contain # The block is usually raw data from the target process. It might contain
@@ -383,15 +390,7 @@ class Child:
ans = list(self.argv) ans = list(self.argv)
return ans return ans
@property def process_desc(self, pid: int) -> ProcessDesc:
def foreground_processes(self) -> list[ProcessDesc]:
if self.child_fd is None:
return []
try:
pgrp = os.tcgetpgrp(self.child_fd)
foreground_processes = processes_in_group(pgrp) if pgrp >= 0 else []
def process_desc(pid: int) -> ProcessDesc:
ans: ProcessDesc = {'pid': pid, 'cmdline': None, 'cwd': None} ans: ProcessDesc = {'pid': pid, 'cmdline': None, 'cwd': None}
with suppress(Exception): with suppress(Exception):
ans['cmdline'] = self.cmdline_of_pid(pid) ans['cmdline'] = self.cmdline_of_pid(pid)
@@ -399,7 +398,38 @@ class Child:
ans['cwd'] = cwd_of_process(pid) or None ans['cwd'] = cwd_of_process(pid) or None
return ans return ans
return [process_desc(x) for x in foreground_processes] @property
def foreground_processes(self) -> list[ProcessDesc]:
if self.child_fd is None:
return []
try:
pgrp = os.tcgetpgrp(self.child_fd)
foreground_processes = processes_in_group(pgrp) if pgrp >= 0 else []
return [self.process_desc(x) for x in foreground_processes]
except Exception:
return []
@property
def background_processes(self) -> list[ProcessDesc]:
if self.child_fd is None:
return []
try:
foreground_process_group_id = os.tcgetpgrp(self.child_fd)
if foreground_process_group_id < 0:
return []
gmap = process_group_map()
sid = session_id(gmap.get(foreground_process_group_id, ()))
if sid < 0:
return []
ans = []
for grp_id, pids in gmap.items():
if grp_id == foreground_process_group_id:
continue
if session_id(pids) == sid:
for pid in pids:
ans.append(self.process_desc(pid))
return ans
except Exception: except Exception:
return [] return []

View File

@@ -1284,8 +1284,8 @@ also applies to requests to quit the entire application (all OS windows, via the
:ac:`quit` action). Negative values are converted to positive ones, however, :ac:`quit` action). Negative values are converted to positive ones, however,
with :opt:`shell_integration` enabled, using negative values means windows with :opt:`shell_integration` enabled, using negative values means windows
sitting at a shell prompt are not counted, only windows where some command is sitting at a shell prompt are not counted, only windows where some command is
currently running. Note that if you want confirmation when closing individual currently running or is running in the background. Note that if you want confirmation
windows, you can map the :ac:`close_window_with_confirmation` action. when closing individual windows, you can map the :ac:`close_window_with_confirmation` action.
''' '''
) )
egr() # }}} egr() # }}}

View File

@@ -300,14 +300,6 @@ class Tab: # {{{
def effective_title(self) -> str: def effective_title(self) -> str:
return self.name or self.title return self.name or self.title
@property
def number_of_windows_with_running_programs(self) -> int:
ans = 0
for window in self:
if window.has_running_program:
ans += 1
return ans
def get_cwd_of_active_window(self, oldest: bool = False) -> str | None: def get_cwd_of_active_window(self, oldest: bool = False) -> str | None:
w = self.active_window w = self.active_window
return w.get_cwd_of_child(oldest) if w else None return w.get_cwd_of_child(oldest) if w else None
@@ -1142,13 +1134,6 @@ class TabManager: # {{{
return t.active_window return t.active_window
return None return None
@property
def number_of_windows_with_running_programs(self) -> int:
count = 0
for tab in self:
count += tab.number_of_windows_with_running_programs
return count
@property @property
def number_of_windows(self) -> int: def number_of_windows(self) -> int:
count = 0 count = 0