Allow using boolean operators when matching windows or tabs
This commit is contained in:
parent
11bc1b100c
commit
ade38870a0
@ -34,6 +34,11 @@ mouse anywhere in the current command to move the cursor there. See
|
||||
Detailed list of changes
|
||||
-------------------------------------
|
||||
|
||||
0.25.1 [future]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Remote control: Allow using :ref:`Boolean operators <search_syntax>` when constructing queries to match windows or tabs
|
||||
|
||||
0.25.0 [2022-04-11]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@ -178,6 +178,24 @@ The remote control protocol
|
||||
If you wish to develop your own client to talk to |kitty|, you
|
||||
can use the :doc:`protocol specification <rc_protocol>`.
|
||||
|
||||
|
||||
.. _search_syntax:
|
||||
|
||||
Matching windows and tabs
|
||||
----------------------------
|
||||
|
||||
Many remote control operations operate on windows or tabs. To select these,
|
||||
the :code:`--match` option is often used. This allows matching using various
|
||||
sophisticated criteria such as title, ids, cmdlines, etc. These criteria are
|
||||
expressions of the form :code:`field:query`. Where field is the field against
|
||||
which to match and query is the expression to match. They can be further
|
||||
combined using Boolean operators, best illustrated with some examples::
|
||||
|
||||
title:"My special window" or id:43
|
||||
title:bash and env:USER=kovid
|
||||
not id:1
|
||||
(id:2 or id:3) and title:something
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ from gettext import gettext as _
|
||||
from time import monotonic
|
||||
from typing import (
|
||||
Any, Callable, Container, Dict, Iterable, Iterator, List, Optional,
|
||||
Sequence, Tuple, Union
|
||||
Sequence, Set, Tuple, Union
|
||||
)
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
@ -62,7 +62,7 @@ from .utils import (
|
||||
platform_window_id, remove_socket_file, safe_print, set_primary_selection,
|
||||
single_instance, startup_notification_handler, which
|
||||
)
|
||||
from .window import CommandOutput, CwdRequest, MatchPatternType, Window
|
||||
from .window import CommandOutput, CwdRequest, Window
|
||||
|
||||
|
||||
class OSWindowDict(TypedDict):
|
||||
@ -340,42 +340,14 @@ class Boss:
|
||||
yield from tab
|
||||
|
||||
def match_windows(self, match: str) -> Iterator[Window]:
|
||||
try:
|
||||
field, exp = match.split(':', 1)
|
||||
except ValueError:
|
||||
return
|
||||
if field == 'num':
|
||||
tab = self.active_tab
|
||||
if tab is not None:
|
||||
try:
|
||||
w = tab.get_nth_window(int(exp))
|
||||
except Exception:
|
||||
return
|
||||
if w is not None:
|
||||
yield w
|
||||
return
|
||||
if field == 'recent':
|
||||
tab = self.active_tab
|
||||
if tab is not None:
|
||||
try:
|
||||
num = int(exp)
|
||||
except Exception:
|
||||
return
|
||||
w = self.window_id_map.get(tab.nth_active_window_id(num))
|
||||
if w is not None:
|
||||
yield w
|
||||
return
|
||||
if field != 'env':
|
||||
pat: MatchPatternType = re.compile(exp)
|
||||
else:
|
||||
kp, vp = exp.partition('=')[::2]
|
||||
if vp:
|
||||
pat = re.compile(kp), re.compile(vp)
|
||||
else:
|
||||
pat = re.compile(kp), None
|
||||
for window in self.all_windows:
|
||||
if window.matches(field, pat):
|
||||
yield window
|
||||
from .search_query_parser import search
|
||||
tab = self.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)}
|
||||
|
||||
for wid in search(match, ('id', 'title', 'pid', 'cwd', 'cmdline', 'num', 'env', 'recent',), set(self.window_id_map), get_matches):
|
||||
yield self.window_id_map[wid]
|
||||
|
||||
def tab_for_window(self, window: Window) -> Optional[Tab]:
|
||||
for tab in self.all_tabs:
|
||||
@ -385,41 +357,18 @@ class Boss:
|
||||
return None
|
||||
|
||||
def match_tabs(self, match: str) -> Iterator[Tab]:
|
||||
try:
|
||||
field, exp = match.split(':', 1)
|
||||
except ValueError:
|
||||
return
|
||||
pat = re.compile(exp)
|
||||
from .search_query_parser import search
|
||||
tm = self.active_tab_manager
|
||||
tim = {t.id: t for t in self.all_tabs}
|
||||
|
||||
def get_matches(location: str, query: str, candidates: Set[int]) -> Set[int]:
|
||||
return {wid for wid in candidates if tim[wid].matches_query(location, query, tm)}
|
||||
|
||||
found = False
|
||||
if field in ('title', 'id'):
|
||||
for tab in self.all_tabs:
|
||||
if tab.matches(field, pat):
|
||||
yield tab
|
||||
found = True
|
||||
elif field in ('window_id', 'window_title'):
|
||||
wf = field.split('_')[1]
|
||||
tabs = {self.tab_for_window(w) for w in self.match_windows(f'{wf}:{exp}')}
|
||||
for q in tabs:
|
||||
if q:
|
||||
found = True
|
||||
yield q
|
||||
elif field == 'index':
|
||||
tm = self.active_tab_manager
|
||||
if tm is not None and len(tm.tabs) > 0:
|
||||
idx = (int(pat.pattern) + len(tm.tabs)) % len(tm.tabs)
|
||||
found = True
|
||||
yield tm.tabs[idx]
|
||||
elif field == 'recent':
|
||||
tm = self.active_tab_manager
|
||||
if tm is not None and len(tm.tabs) > 0:
|
||||
try:
|
||||
num = int(exp)
|
||||
except Exception:
|
||||
return
|
||||
q = tm.nth_active_tab(num)
|
||||
if q is not None:
|
||||
found = True
|
||||
yield q
|
||||
for tid in search(match, ('id', 'index', 'title', 'window_id', 'window_title', 'pid', 'cwd', 'env', 'cmdline', 'recent',), set(tim), get_matches):
|
||||
found = True
|
||||
yield tim[tid]
|
||||
|
||||
if not found:
|
||||
tabs = {self.tab_for_window(w) for w in self.match_windows(match)}
|
||||
for q in tabs:
|
||||
|
||||
@ -151,7 +151,7 @@ def doc(x: str) -> str:
|
||||
|
||||
@role
|
||||
def ref(x: str) -> str:
|
||||
return re.sub(r'\s*<.+?>', '', x)
|
||||
return re.sub(r'\s*<\S+?>', '', x)
|
||||
|
||||
|
||||
OptionSpecSeq = List[Union[str, OptionDict]]
|
||||
|
||||
@ -77,7 +77,8 @@ MATCH_WINDOW_OPTION = '''\
|
||||
--match -m
|
||||
The window to match. Match specifications are of the form:
|
||||
:italic:`field:regexp`. Where field can be one of: id, title, pid, cwd, cmdline, num, env and recent.
|
||||
You can use the :italic:`ls` command to get a list of windows. Note that for
|
||||
You can use the :italic:`ls` command to get a list of windows. Expressions can
|
||||
be :ref:`combined using Boolean operators <search_syntax>`. Note that for
|
||||
numeric fields such as id, pid, recent and num the expression is interpreted as a number,
|
||||
not a regular expression. The field num refers to the window position in the current tab,
|
||||
starting from zero and counting clockwise (this is the same as the order in which the
|
||||
@ -93,7 +94,8 @@ MATCH_TAB_OPTION = '''\
|
||||
The tab to match. Match specifications are of the form:
|
||||
:italic:`field:regexp`. Where field can be one of:
|
||||
id, index, title, window_id, window_title, pid, cwd, env, cmdline and recent.
|
||||
You can use the :italic:`ls` command to get a list of tabs. Note that for
|
||||
You can use the :italic:`ls` command to get a list of tabs. Expressions can
|
||||
be :ref:`combined using Boolean operators <search_syntax>`. Note that for
|
||||
numeric fields such as id, recent, index and pid the expression is interpreted as a number,
|
||||
not a regular expression. When using title or id, first a matching tab is
|
||||
looked for and if not found a matching window is looked for, and the tab
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import weakref
|
||||
from collections import deque
|
||||
@ -10,7 +11,7 @@ from operator import attrgetter
|
||||
from time import monotonic
|
||||
from typing import (
|
||||
Any, Deque, Dict, Generator, Iterable, Iterator, List, NamedTuple,
|
||||
Optional, Pattern, Sequence, Set, Tuple, Union
|
||||
Optional, Sequence, Set, Tuple, Union
|
||||
)
|
||||
|
||||
from .borders import Border, Borders
|
||||
@ -687,11 +688,26 @@ class Tab: # {{{
|
||||
for w in self:
|
||||
yield w.as_dict(is_focused=w is active_window, is_self=w is self_window)
|
||||
|
||||
def matches(self, field: str, pat: 'Pattern[str]') -> bool:
|
||||
if field == 'id':
|
||||
return bool(pat.pattern == str(self.id))
|
||||
def matches_query(self, field: str, query: str, active_tab_manager: Optional['TabManager'] = None) -> bool:
|
||||
if field == 'title':
|
||||
return pat.search(self.effective_title) is not None
|
||||
return re.search(query, self.effective_title) is not None
|
||||
if field == 'id':
|
||||
return query == str(self.id)
|
||||
if field in ('window_id', 'window_title'):
|
||||
field = field.partition('_')[-1]
|
||||
for w in self:
|
||||
if w.matches_query(field, query):
|
||||
return True
|
||||
return False
|
||||
if field == 'index':
|
||||
if active_tab_manager and len(active_tab_manager.tabs):
|
||||
idx = (int(query) + len(active_tab_manager.tabs)) % len(active_tab_manager.tabs)
|
||||
return active_tab_manager.tabs[idx] is self
|
||||
return False
|
||||
if field == 'recent':
|
||||
if active_tab_manager and len(active_tab_manager.tabs):
|
||||
return self is active_tab_manager.nth_active_tab(int(query))
|
||||
return False
|
||||
return False
|
||||
|
||||
def __iter__(self) -> Iterator[Window]:
|
||||
|
||||
@ -7,8 +7,9 @@ import re
|
||||
import sys
|
||||
import weakref
|
||||
from collections import deque
|
||||
from contextlib import suppress
|
||||
from enum import Enum, IntEnum, auto
|
||||
from functools import partial
|
||||
from functools import lru_cache, partial
|
||||
from gettext import gettext as _
|
||||
from itertools import chain
|
||||
from time import monotonic
|
||||
@ -20,7 +21,7 @@ from typing import (
|
||||
from .child import ProcessDesc
|
||||
from .cli_stub import CLIOptions
|
||||
from .config import build_ansi_color_table
|
||||
from .constants import appname, is_macos, wakeup, config_dir
|
||||
from .constants import appname, config_dir, is_macos, wakeup
|
||||
from .fast_data_types import (
|
||||
BGIMAGE_PROGRAM, BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM,
|
||||
CELL_PROGRAM, CELL_SPECIAL_PROGRAM, CURSOR_BEAM, CURSOR_BLOCK,
|
||||
@ -113,6 +114,19 @@ def process_title_from_child(title: str, is_base64: bool) -> str:
|
||||
return sanitize_title(title)
|
||||
|
||||
|
||||
@lru_cache(maxsize=64)
|
||||
def compile_match_query(exp: str, is_simple: bool = True) -> MatchPatternType:
|
||||
if is_simple:
|
||||
pat: MatchPatternType = re.compile(exp)
|
||||
else:
|
||||
kp, vp = exp.partition('=')[::2]
|
||||
if vp:
|
||||
pat = re.compile(kp), re.compile(vp)
|
||||
else:
|
||||
pat = re.compile(kp), None
|
||||
return pat
|
||||
|
||||
|
||||
class WindowDict(TypedDict):
|
||||
id: int
|
||||
is_focused: bool
|
||||
@ -621,9 +635,9 @@ class Window:
|
||||
assert not isinstance(pat, tuple)
|
||||
|
||||
if field in ('id', 'window_id'):
|
||||
return True if pat.pattern == str(self.id) else False
|
||||
return pat.pattern == str(self.id)
|
||||
if field == 'pid':
|
||||
return True if pat.pattern == str(self.child.pid) else False
|
||||
return pat.pattern == str(self.child.pid)
|
||||
if field == 'title':
|
||||
return pat.search(self.override_title or self.title) is not None
|
||||
if field in 'cwd':
|
||||
@ -635,6 +649,21 @@ class Window:
|
||||
return False
|
||||
return False
|
||||
|
||||
def matches_query(self, field: str, query: str, active_tab: Optional[TabType] = None) -> bool:
|
||||
if field in ('num', 'recent'):
|
||||
if active_tab is not None:
|
||||
try:
|
||||
q = int(query)
|
||||
except Exception:
|
||||
return False
|
||||
with suppress(Exception):
|
||||
if field == 'num':
|
||||
return active_tab.get_nth_window(q) is self
|
||||
return active_tab.nth_active_window_id(q) == self.id
|
||||
return False
|
||||
pat = compile_match_query(query, field != 'env')
|
||||
return self.matches(field, pat)
|
||||
|
||||
def set_visible_in_layout(self, val: bool) -> None:
|
||||
val = bool(val)
|
||||
if val is not self.is_visible_in_layout:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user