diff --git a/docs/changelog.rst b/docs/changelog.rst index 3e019a419..4e42f5db3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -54,7 +54,7 @@ Detailed list of changes - Wayland: Fix high CPU usage when using some input methods (:pull:`5369`) -- Remote control: When matching window by `state:focused` match the window belonging to the OS window that was last focused (:iss:`5602`) +- Remote control: When matching window by `state:focused` and no window currently has keyboard focus, match the window belonging to the OS window that was last focused (:iss:`5602`) 0.26.4 [2022-10-17] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/boss.py b/kitty/boss.py index b055f34eb..a3aa1438c 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -12,8 +12,8 @@ from functools import partial from gettext import gettext as _ from time import monotonic from typing import ( - TYPE_CHECKING, Any, Callable, Container, Dict, Iterable, Iterator, List, - Optional, Set, Tuple, Union + TYPE_CHECKING, Any, Callable, Container, Dict, Iterable, Iterator, List, Optional, + Set, Tuple, Union, ) from weakref import WeakValueDictionary @@ -21,15 +21,15 @@ from .child import cached_process_data, default_env, set_default_env from .cli import create_opts, parse_args from .cli_stub import CLIOptions from .clipboard import ( - Clipboard, get_clipboard_string, get_primary_selection, - set_clipboard_string, set_primary_selection + Clipboard, get_clipboard_string, get_primary_selection, set_clipboard_string, + set_primary_selection, ) from .conf.utils import BadLine, KeyAction, to_cmdline from .config import common_opts_as_dict, prepare_config_file_for_editing from .constants import ( RC_ENCRYPTION_PROTOCOL_VERSION, appname, cache_dir, clear_handled_signals, - config_dir, handled_signals, is_macos, is_wayland, kitty_exe, - logo_png_file, supports_primary_selection, website_url + config_dir, handled_signals, is_macos, is_wayland, kitty_exe, logo_png_file, + supports_primary_selection, website_url, ) from .fast_data_types import ( CLOSE_BEING_CONFIRMED, GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_SHIFT, @@ -37,15 +37,15 @@ from .fast_data_types import ( IMPERATIVE_CLOSE_REQUESTED, NO_CLOSE_REQUESTED, ChildMonitor, Color, EllipticCurveKey, KeyEvent, SingleKey, add_timer, apply_options_update, background_opacity_of, change_background_opacity, change_os_window_state, - cocoa_set_menubar_title, create_os_window, last_focused_os_window_id, - current_application_quit_request, current_os_window, destroy_global_data, - focus_os_window, get_boss, get_options, get_os_window_size, - global_font_size, mark_os_window_for_close, os_window_font_size, - patch_global_colors, redirect_mouse_handling, ring_bell, - run_with_activation_token, safe_pipe, send_data_to_peer, - set_application_quit_request, set_background_image, set_boss, - set_in_sequence_mode, set_options, set_os_window_size, set_os_window_title, - thread_write, toggle_fullscreen, toggle_maximized, toggle_secure_input + cocoa_set_menubar_title, create_os_window, current_application_quit_request, + current_focused_os_window_id, current_os_window, destroy_global_data, + focus_os_window, get_boss, get_options, get_os_window_size, global_font_size, + last_focused_os_window_id, mark_os_window_for_close, os_window_font_size, + patch_global_colors, redirect_mouse_handling, ring_bell, run_with_activation_token, + safe_pipe, send_data_to_peer, set_application_quit_request, set_background_image, + set_boss, set_in_sequence_mode, set_options, set_os_window_size, + set_os_window_title, thread_write, toggle_fullscreen, toggle_maximized, + toggle_secure_input, ) from .key_encoding import get_name_to_functional_number_map from .keys import get_shortcut, shortcut_matches @@ -57,16 +57,14 @@ from .os_window_size import initial_window_size_func from .prewarm import PrewarmProcess from .rgb import color_from_int from .session import Session, create_sessions, get_os_window_sizing_data -from .tabs import ( - SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager -) +from .tabs import SpecialWindow, SpecialWindowInstance, Tab, TabDict, TabManager from .types import _T, AsyncResponse, WindowSystemMouseEvent, ac from .typing import PopenType, TypedDict from .utils import ( cleanup_ssh_control_masters, func_name, get_editor, get_new_os_window_size, is_path_in_temp_dir, less_version, log_error, macos_version, open_url, parse_address_spec, parse_uri_list, platform_window_id, remove_socket_file, - safe_print, single_instance, startup_notification_handler, which + safe_print, single_instance, startup_notification_handler, which, ) from .window import CommandOutput, CwdRequest, Window @@ -80,6 +78,8 @@ class OSWindowDict(TypedDict): id: int platform_window_id: Optional[int] is_focused: bool + is_active: bool + last_focused: bool tabs: List[TabDict] wm_class: str wm_name: str @@ -287,9 +287,7 @@ class Boss: self.mouse_handler: Optional[Callable[[WindowSystemMouseEvent], None]] = None self.update_keymap() if is_macos: - from .fast_data_types import ( - cocoa_set_notification_activated_callback - ) + from .fast_data_types import cocoa_set_notification_activated_callback cocoa_set_notification_activated_callback(notification_activated) def update_keymap(self) -> None: @@ -351,7 +349,9 @@ class Boss: yield { 'id': os_window_id, 'platform_window_id': platform_window_id(os_window_id), - 'is_focused': tm is active_tab_manager and os_window_id == last_focused_os_window_id(), + 'is_active': tm is active_tab_manager, + 'is_focused': current_focused_os_window_id() == os_window_id, + 'last_focused': os_window_id == last_focused_os_window_id(), 'tabs': list(tm.list_tabs(active_tab, active_window, self_window)), 'wm_class': tm.wm_class, 'wm_name': tm.wm_name @@ -377,6 +377,10 @@ class Boss: return from .search_query_parser import search tab = self.active_tab + if current_focused_os_window_id() <= 0: + tm = self.os_window_map.get(last_focused_os_window_id()) + if tm is not None: + tab = tm.active_tab def get_matches(location: str, query: str, candidates: Set[int]) -> Set[int]: return {wid for wid in candidates if self.window_id_map[wid].matches_query(location, query, tab)} @@ -398,6 +402,8 @@ class Boss: return self.all_tabs from .search_query_parser import search tm = self.active_tab_manager + if current_focused_os_window_id() <= 0: + tm = self.os_window_map.get(last_focused_os_window_id()) or tm tim = {t.id: t for t in self.all_tabs} def get_matches(location: str, query: str, candidates: Set[int]) -> Set[int]: @@ -534,9 +540,7 @@ class Boss: return True def remote_cmd_permission_received(self, pcmd: Dict[str, Any], window_id: int, peer_id: int, choice: str) -> None: - from .remote_control import ( - encode_response_for_peer, set_user_password_allowed - ) + from .remote_control import encode_response_for_peer, set_user_password_allowed response: RCResponse = None window = self.window_id_map.get(window_id) choice = choice or 'r' @@ -587,9 +591,7 @@ class Boss: self.show_error(_('remote_control mapping failed'), tb) def call_remote_control(self, active_window: Optional[Window], args: Tuple[str, ...]) -> 'ResponseType': - from .rc.base import ( - PayloadGetter, command_for_name, parse_subcommand_cli - ) + from .rc.base import PayloadGetter, command_for_name, parse_subcommand_cli from .remote_control import parse_rc_args aa = list(args) silent = False @@ -2248,9 +2250,7 @@ class Boss: log_error(f'Failed to process update check data {raw!r}, with error: {e}') def dbus_notification_callback(self, activated: bool, a: int, b: Union[int, str]) -> None: - from .notify import ( - dbus_notification_activated, dbus_notification_created - ) + from .notify import dbus_notification_activated, dbus_notification_created if activated: assert isinstance(b, str) dbus_notification_activated(a, b) @@ -2284,9 +2284,7 @@ class Boss: map f5 set_colors --configured /path/to/some/config/file/colors.conf ''') def set_colors(self, *args: str) -> None: - from kitty.rc.base import ( - PayloadGetter, command_for_name, parse_subcommand_cli - ) + from kitty.rc.base import PayloadGetter, command_for_name, parse_subcommand_cli from kitty.remote_control import parse_rc_args c = command_for_name('set_colors') try: diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 2b885333d..9ea11708c 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -790,6 +790,10 @@ def last_focused_os_window_id() -> int: pass +def current_focused_os_window_id() -> int: + pass + + def cocoa_set_menubar_title(title: str) -> None: pass diff --git a/kitty/rc/base.py b/kitty/rc/base.py index 183b95cab..3fc94341f 100644 --- a/kitty/rc/base.py +++ b/kitty/rc/base.py @@ -100,7 +100,7 @@ or a name and value, for example, :code:`env:MY_ENV_VAR=2`. The field :code:`state` matches on the state of the window. Supported states are: :code:`active`, :code:`focused`, :code:`needs_attention`, :code:`parent_active` and :code:`parent_focused`. Active windows are the windows that are active in their parent tab. There is only one focused window and it is the -window to which keyboard events are delivered. +window to which keyboard events are delivered. If no window is focused, the last focused window is matched. Note that you can use the :ref:`kitty @ ls ` command to get a list of windows. ''' @@ -134,7 +134,7 @@ variables are matched. The field :code:`state` matches on the state of the tab. Supported states are: :code:`active`, :code:`focused`, :code:`needs_attention`, :code:`parent_active` and :code:`parent_focused`. Active tabs are the tabs that are active in their parent OS window. There is only one focused tab -and it is the tab to which keyboard events are delivered. +and it is the tab to which keyboard events are delivered. If no tab is focused, the last focused tab is matched. Note that you can use the :ref:`kitty @ ls ` command to get a list of tabs. ''' diff --git a/kitty/state.c b/kitty/state.c index b0e6f68d9..c8e165728 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -117,6 +117,16 @@ last_focused_os_window_id(void) { } +static id_type +current_focused_os_window_id(void) { + for (size_t i = 0; i < global_state.num_os_windows; i++) { + OSWindow *w = &global_state.os_windows[i]; + if (w->is_focused) { return w->id; } + } + return 0; +} + + OSWindow* os_window_for_kitty_window(id_type kitty_window_id) { for (size_t i = 0; i < global_state.num_os_windows; i++) { @@ -682,6 +692,10 @@ PYWRAP0(last_focused_os_window_id) { return PyLong_FromUnsignedLongLong(last_focused_os_window_id()); } +PYWRAP0(current_focused_os_window_id) { + return PyLong_FromUnsignedLongLong(current_focused_os_window_id()); +} + PYWRAP1(handle_for_window_id) { id_type os_window_id; @@ -1285,6 +1299,7 @@ static PyMethodDef module_methods[] = { MW(current_os_window, METH_NOARGS), MW(next_window_id, METH_NOARGS), MW(last_focused_os_window_id, METH_NOARGS), + MW(current_focused_os_window_id, METH_NOARGS), MW(set_options, METH_VARARGS), MW(get_options, METH_NOARGS), MW(click_mouse_url, METH_VARARGS), diff --git a/kitty/tabs.py b/kitty/tabs.py index e09dbbc49..e6c101edb 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -20,10 +20,10 @@ from .cli_stub import CLIOptions from .constants import appname, kitty_exe from .fast_data_types import ( GLFW_MOUSE_BUTTON_LEFT, GLFW_MOUSE_BUTTON_MIDDLE, GLFW_PRESS, GLFW_RELEASE, add_tab, - attach_window, current_os_window, detach_window, get_boss, get_click_interval, - get_options, last_focused_os_window_id, mark_tab_bar_dirty, next_window_id, - remove_tab, remove_window, ring_bell, set_active_tab, set_active_window, swap_tabs, - sync_os_window_title, + attach_window, current_focused_os_window_id, detach_window, get_boss, + get_click_interval, get_options, last_focused_os_window_id, mark_tab_bar_dirty, + next_window_id, remove_tab, remove_window, ring_bell, set_active_tab, + set_active_window, swap_tabs, sync_os_window_title, ) from .layout.base import Layout from .layout.interface import create_layout_object_for, evict_cached_layouts @@ -701,7 +701,10 @@ class Tab: # {{{ def list_windows(self, active_window: Optional[Window], self_window: Optional[Window] = None) -> Generator[WindowDict, None, None]: for w in self: - yield w.as_dict(is_focused=w is active_window and w.os_window_id == last_focused_os_window_id(), is_self=w is self_window) + yield w.as_dict( + is_active_window=w is active_window, + is_focused=w.os_window_id == current_focused_os_window_id() and w is active_window, + is_self=w is self_window) def matches_query(self, field: str, query: str, active_tab_manager: Optional['TabManager'] = None) -> bool: if field == 'title': @@ -725,7 +728,8 @@ class Tab: # {{{ return False if field == 'state': if query == 'active': - return active_tab_manager is not None and self is active_tab_manager.active_tab + tm = self.tab_manager_ref() + return tm is not None and self is tm.active_tab if query == 'focused': return active_tab_manager is not None and self is active_tab_manager.active_tab and self.os_window_id == last_focused_os_window_id() if query == 'needs_attention': @@ -735,7 +739,7 @@ class Tab: # {{{ if query == 'parent_active': return active_tab_manager is not None and self.tab_manager_ref() is active_tab_manager if query == 'parent_focused': - return active_tab_manager is not None and self.tab_manager_ref() is active_tab_manager and self.os_window_id == current_os_window() + return active_tab_manager is not None and self.tab_manager_ref() is active_tab_manager and self.os_window_id == last_focused_os_window_id() return False return False @@ -939,7 +943,7 @@ class TabManager: # {{{ for tab in self: yield { 'id': tab.id, - 'is_focused': tab is active_tab and tab.os_window_id == last_focused_os_window_id(), + 'is_focused': tab is active_tab and tab.os_window_id == current_focused_os_window_id(), 'is_active_tab': tab is active_tab, 'title': tab.name or tab.title, 'layout': str(tab.current_layout.name), diff --git a/kitty/window.py b/kitty/window.py index ae2377e40..fcbe1f03b 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -707,15 +707,20 @@ class Window: return False if field == 'state': if query == 'active': - return active_tab is not None and self is active_tab.active_window + tab = self.tabref() + return tab is not None and tab.active_window is self if query == 'focused': return active_tab is not None and self is active_tab.active_window and last_focused_os_window_id() == self.os_window_id if query == 'needs_attention': return self.needs_attention if query == 'parent_active': - return active_tab is not None and self.tabref() is active_tab + tab = self.tabref() + if tab is not None: + tm = tab.tab_manager_ref() + return tm is not None and tm.active_tab is tab + return False if query == 'parent_focused': - return active_tab is not None and self.tabref() is active_tab and current_os_window() == self.os_window_id + return active_tab is not None and self.tabref() is active_tab and last_focused_os_window_id() == self.os_window_id return False pat = compile_match_query(query, field != 'env') return self.matches(field, pat)