From cade8efc25f7a0fdd29d26ccf51718238f8a2daf Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 3 Aug 2023 11:53:17 +0530 Subject: [PATCH] kitten @ ls: Allow limiting output to matched windows/tabs Fixes #6520 --- docs/changelog.rst | 2 ++ kitty/boss.py | 28 +++++++++++++++++----------- kitty/rc/base.py | 4 ++-- kitty/rc/ls.py | 38 ++++++++++++++++++++++++++++++-------- kitty/tabs.py | 45 +++++++++++++++++++++++++++------------------ 5 files changed, 78 insertions(+), 39 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2b1646d3b..40a3911e5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -45,6 +45,8 @@ Detailed list of changes - kitten @ ls: Add user variables set on windows to the output (:iss:`6502`) +- kitten @ ls: Allow limiting output to matched windows/tabs (:iss:`6520`) + - X11: Print an error to STDERR instead of refusing to start when the user sets a custom window icon larger than 128128 (:iss:`6507`) 0.29.2 [2023-07-27] diff --git a/kitty/boss.py b/kitty/boss.py index 7103df35b..b050b8952 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -428,20 +428,26 @@ class Boss: self.os_window_map[os_window_id] = tm return os_window_id - def list_os_windows(self, self_window: Optional[Window] = None) -> Iterator[OSWindowDict]: + def list_os_windows( + self, self_window: Optional[Window] = None, + tab_filter: Optional[Callable[[Tab], bool]] = None, + window_filter: Optional[Callable[[Window], bool]] = None + ) -> Iterator[OSWindowDict]: with cached_process_data(): active_tab_manager = self.active_tab_manager for os_window_id, tm in self.os_window_map.items(): - yield { - 'id': os_window_id, - 'platform_window_id': platform_window_id(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(self_window)), - 'wm_class': tm.wm_class, - 'wm_name': tm.wm_name - } + tabs = list(tm.list_tabs(self_window, tab_filter, window_filter)) + if tabs: + yield { + 'id': os_window_id, + 'platform_window_id': platform_window_id(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': tabs, + 'wm_class': tm.wm_class, + 'wm_name': tm.wm_name + } @property def all_tab_managers(self) -> Iterator[TabManager]: diff --git a/kitty/rc/base.py b/kitty/rc/base.py index b0b5bc5cd..b573e002f 100644 --- a/kitty/rc/base.py +++ b/kitty/rc/base.py @@ -13,11 +13,11 @@ from kitty.types import AsyncResponse if TYPE_CHECKING: from kitty.boss import Boss as B - from kitty.tabs import Tab + from kitty.tabs import Tab as T from kitty.window import Window as W Window = W Boss = B - Tab + Tab = T else: Boss = Window = Tab = None RCOptions = R diff --git a/kitty/rc/ls.py b/kitty/rc/ls.py index bbc9bf7d0..73cdb930d 100644 --- a/kitty/rc/ls.py +++ b/kitty/rc/ls.py @@ -2,11 +2,11 @@ # License: GPLv3 Copyright: 2020, Kovid Goyal import json -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Set, Tuple from kitty.constants import appname -from .base import ArgsType, Boss, PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType, Window +from .base import MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType, Tab, Window if TYPE_CHECKING: from kitty.cli_stub import LSRCOptions as CLIOptions @@ -15,29 +15,51 @@ if TYPE_CHECKING: class LS(RemoteCommand): protocol_spec = __doc__ = ''' all_env_vars/bool: Whether to send all environment variables for every window rather than just differing ones + match/str: Window to change colors in + match_tab/str: Tab to change colors in + self/bool: Boolean indicating whether to list only the window the command is run in ''' - short_desc = 'List all tabs/windows' + short_desc = 'List tabs/windows' desc = ( - 'List all windows. The list is returned as JSON tree. The top-level is a list of' + 'List windows. The list is returned as JSON tree. The top-level is a list of' f' operating system {appname} windows. Each OS window has an :italic:`id` and a list' ' of :italic:`tabs`. Each tab has its own :italic:`id`, a :italic:`title` and a list of :italic:`windows`.' ' Each window has an :italic:`id`, :italic:`title`, :italic:`current working directory`, :italic:`process id (PID)`,' ' :italic:`command-line` and :italic:`environment` of the process running in the window. Additionally, when' ' running the command inside a kitty window, that window can be identified by the :italic:`is_self` parameter.\n\n' - 'You can use these criteria to select windows/tabs for the other commands.' + 'You can use these criteria to select windows/tabs for the other commands.\n\n' + 'You can limit the windows/tabs in the output by using the :option:`--match` and :option:`--match-tab` options.' ) options_spec = '''\ --all-env-vars type=bool-set Show all environment variables in output, not just differing ones. -''' + + +--self +type=bool-set +Only list the window this command is run in. +''' + '\n\n' + MATCH_WINDOW_OPTION + '\n\n' + MATCH_TAB_OPTION.replace('--match -m', '--match-tab -t', 1) def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType: - return {'all_env_vars': opts.all_env_vars} + return {'all_env_vars': opts.all_env_vars, 'match': opts.match, 'match_tab': opts.match_tab} def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType: - data = list(boss.list_os_windows(window)) + tab_filter: Optional[Callable[[Tab], bool]] = None + window_filter: Optional[Callable[[Window], bool]] = None + + if payload_get('match') is not None: + window_ids = frozenset(w.id for w in self.windows_for_match_payload(boss, window, payload_get)) + def wf(w: Window) -> bool: + return w.id in window_ids + window_filter = wf + if payload_get('match_tab') is not None: + tab_ids = frozenset(w.id for w in self.tabs_for_match_payload(boss, window, payload_get)) + def tf(w: Tab) -> bool: + return w.id in tab_ids + tab_filter = tf + data = list(boss.list_os_windows(window, tab_filter, window_filter)) if not payload_get('all_env_vars'): all_env_blocks: List[Dict[str, str]] = [] common_env_vars: Set[Tuple[str, str]] = set() diff --git a/kitty/tabs.py b/kitty/tabs.py index 6ada6fe8d..efe34ec76 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -11,6 +11,7 @@ from operator import attrgetter from time import monotonic from typing import ( Any, + Callable, Deque, Dict, Generator, @@ -770,13 +771,14 @@ class Tab: # {{{ def move_window_backward(self) -> None: self.move_window(-1) - def list_windows(self, self_window: Optional[Window] = None) -> Generator[WindowDict, None, None]: + def list_windows(self, self_window: Optional[Window] = None, window_filter: Optional[Callable[[Window], bool]] = None) -> Generator[WindowDict, None, None]: active_window = self.active_window for w in self: - yield w.as_dict( - is_active=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) + if window_filter is None or window_filter(w): + yield w.as_dict( + is_active=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': @@ -1011,21 +1013,28 @@ class TabManager: # {{{ def __len__(self) -> int: return len(self.tabs) - def list_tabs(self, self_window: Optional[Window] = None) -> Generator[TabDict, None, None]: + def list_tabs( + self, self_window: Optional[Window] = None, + tab_filter: Optional[Callable[[Tab], bool]] = None, + window_filter: Optional[Callable[[Window], bool]] = None + ) -> Generator[TabDict, None, None]: active_tab = self.active_tab for tab in self: - yield { - 'id': tab.id, - 'is_focused': tab is active_tab and tab.os_window_id == current_focused_os_window_id(), - 'is_active': tab is active_tab, - 'title': tab.name or tab.title, - 'layout': str(tab.current_layout.name), - 'layout_state': tab.current_layout.layout_state(), - 'layout_opts': tab.current_layout.layout_opts.serialized(), - 'enabled_layouts': tab.enabled_layouts, - 'windows': list(tab.list_windows(self_window)), - 'active_window_history': list(tab.windows.active_window_history), - } + if tab_filter is None or tab_filter(tab): + windows = list(tab.list_windows(self_window, window_filter)) + if windows: + yield { + 'id': tab.id, + 'is_focused': tab is active_tab and tab.os_window_id == current_focused_os_window_id(), + 'is_active': tab is active_tab, + 'title': tab.name or tab.title, + 'layout': str(tab.current_layout.name), + 'layout_state': tab.current_layout.layout_state(), + 'layout_opts': tab.current_layout.layout_opts.serialized(), + 'enabled_layouts': tab.enabled_layouts, + 'windows': windows, + 'active_window_history': list(tab.windows.active_window_history), + } def serialize_state(self) -> Dict[str, Any]: return {