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
|
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]
|
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
|
If you wish to develop your own client to talk to |kitty|, you
|
||||||
can use the :doc:`protocol specification <rc_protocol>`.
|
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::
|
.. toctree::
|
||||||
:hidden:
|
:hidden:
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from gettext import gettext as _
|
|||||||
from time import monotonic
|
from time import monotonic
|
||||||
from typing import (
|
from typing import (
|
||||||
Any, Callable, Container, Dict, Iterable, Iterator, List, Optional,
|
Any, Callable, Container, Dict, Iterable, Iterator, List, Optional,
|
||||||
Sequence, Tuple, Union
|
Sequence, Set, Tuple, Union
|
||||||
)
|
)
|
||||||
from weakref import WeakValueDictionary
|
from weakref import WeakValueDictionary
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ from .utils import (
|
|||||||
platform_window_id, remove_socket_file, safe_print, set_primary_selection,
|
platform_window_id, remove_socket_file, safe_print, set_primary_selection,
|
||||||
single_instance, startup_notification_handler, which
|
single_instance, startup_notification_handler, which
|
||||||
)
|
)
|
||||||
from .window import CommandOutput, CwdRequest, MatchPatternType, Window
|
from .window import CommandOutput, CwdRequest, Window
|
||||||
|
|
||||||
|
|
||||||
class OSWindowDict(TypedDict):
|
class OSWindowDict(TypedDict):
|
||||||
@ -340,42 +340,14 @@ class Boss:
|
|||||||
yield from tab
|
yield from tab
|
||||||
|
|
||||||
def match_windows(self, match: str) -> Iterator[Window]:
|
def match_windows(self, match: str) -> Iterator[Window]:
|
||||||
try:
|
from .search_query_parser import search
|
||||||
field, exp = match.split(':', 1)
|
tab = self.active_tab
|
||||||
except ValueError:
|
|
||||||
return
|
def get_matches(location: str, query: str, candidates: Set[int]) -> Set[int]:
|
||||||
if field == 'num':
|
return {wid for wid in candidates if self.window_id_map[wid].matches_query(location, query, tab)}
|
||||||
tab = self.active_tab
|
|
||||||
if tab is not None:
|
for wid in search(match, ('id', 'title', 'pid', 'cwd', 'cmdline', 'num', 'env', 'recent',), set(self.window_id_map), get_matches):
|
||||||
try:
|
yield self.window_id_map[wid]
|
||||||
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
|
|
||||||
|
|
||||||
def tab_for_window(self, window: Window) -> Optional[Tab]:
|
def tab_for_window(self, window: Window) -> Optional[Tab]:
|
||||||
for tab in self.all_tabs:
|
for tab in self.all_tabs:
|
||||||
@ -385,41 +357,18 @@ class Boss:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def match_tabs(self, match: str) -> Iterator[Tab]:
|
def match_tabs(self, match: str) -> Iterator[Tab]:
|
||||||
try:
|
from .search_query_parser import search
|
||||||
field, exp = match.split(':', 1)
|
tm = self.active_tab_manager
|
||||||
except ValueError:
|
tim = {t.id: t for t in self.all_tabs}
|
||||||
return
|
|
||||||
pat = re.compile(exp)
|
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
|
found = False
|
||||||
if field in ('title', 'id'):
|
for tid in search(match, ('id', 'index', 'title', 'window_id', 'window_title', 'pid', 'cwd', 'env', 'cmdline', 'recent',), set(tim), get_matches):
|
||||||
for tab in self.all_tabs:
|
found = True
|
||||||
if tab.matches(field, pat):
|
yield tim[tid]
|
||||||
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
|
|
||||||
if not found:
|
if not found:
|
||||||
tabs = {self.tab_for_window(w) for w in self.match_windows(match)}
|
tabs = {self.tab_for_window(w) for w in self.match_windows(match)}
|
||||||
for q in tabs:
|
for q in tabs:
|
||||||
|
|||||||
@ -151,7 +151,7 @@ def doc(x: str) -> str:
|
|||||||
|
|
||||||
@role
|
@role
|
||||||
def ref(x: str) -> str:
|
def ref(x: str) -> str:
|
||||||
return re.sub(r'\s*<.+?>', '', x)
|
return re.sub(r'\s*<\S+?>', '', x)
|
||||||
|
|
||||||
|
|
||||||
OptionSpecSeq = List[Union[str, OptionDict]]
|
OptionSpecSeq = List[Union[str, OptionDict]]
|
||||||
|
|||||||
@ -77,7 +77,8 @@ MATCH_WINDOW_OPTION = '''\
|
|||||||
--match -m
|
--match -m
|
||||||
The window to match. Match specifications are of the form:
|
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.
|
: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,
|
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,
|
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
|
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:
|
The tab to match. Match specifications are of the form:
|
||||||
:italic:`field:regexp`. Where field can be one of:
|
:italic:`field:regexp`. Where field can be one of:
|
||||||
id, index, title, window_id, window_title, pid, cwd, env, cmdline and recent.
|
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,
|
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
|
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
|
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>
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import stat
|
import stat
|
||||||
import weakref
|
import weakref
|
||||||
from collections import deque
|
from collections import deque
|
||||||
@ -10,7 +11,7 @@ from operator import attrgetter
|
|||||||
from time import monotonic
|
from time import monotonic
|
||||||
from typing import (
|
from typing import (
|
||||||
Any, Deque, Dict, Generator, Iterable, Iterator, List, NamedTuple,
|
Any, Deque, Dict, Generator, Iterable, Iterator, List, NamedTuple,
|
||||||
Optional, Pattern, Sequence, Set, Tuple, Union
|
Optional, Sequence, Set, Tuple, Union
|
||||||
)
|
)
|
||||||
|
|
||||||
from .borders import Border, Borders
|
from .borders import Border, Borders
|
||||||
@ -687,11 +688,26 @@ class Tab: # {{{
|
|||||||
for w in self:
|
for w in self:
|
||||||
yield w.as_dict(is_focused=w is active_window, is_self=w is self_window)
|
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:
|
def matches_query(self, field: str, query: str, active_tab_manager: Optional['TabManager'] = None) -> bool:
|
||||||
if field == 'id':
|
|
||||||
return bool(pat.pattern == str(self.id))
|
|
||||||
if field == 'title':
|
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
|
return False
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[Window]:
|
def __iter__(self) -> Iterator[Window]:
|
||||||
|
|||||||
@ -7,8 +7,9 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import weakref
|
import weakref
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from contextlib import suppress
|
||||||
from enum import Enum, IntEnum, auto
|
from enum import Enum, IntEnum, auto
|
||||||
from functools import partial
|
from functools import lru_cache, partial
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from time import monotonic
|
from time import monotonic
|
||||||
@ -20,7 +21,7 @@ from typing import (
|
|||||||
from .child import ProcessDesc
|
from .child import ProcessDesc
|
||||||
from .cli_stub import CLIOptions
|
from .cli_stub import CLIOptions
|
||||||
from .config import build_ansi_color_table
|
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 (
|
from .fast_data_types import (
|
||||||
BGIMAGE_PROGRAM, BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM,
|
BGIMAGE_PROGRAM, BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM,
|
||||||
CELL_PROGRAM, CELL_SPECIAL_PROGRAM, CURSOR_BEAM, CURSOR_BLOCK,
|
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)
|
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):
|
class WindowDict(TypedDict):
|
||||||
id: int
|
id: int
|
||||||
is_focused: bool
|
is_focused: bool
|
||||||
@ -621,9 +635,9 @@ class Window:
|
|||||||
assert not isinstance(pat, tuple)
|
assert not isinstance(pat, tuple)
|
||||||
|
|
||||||
if field in ('id', 'window_id'):
|
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':
|
if field == 'pid':
|
||||||
return True if pat.pattern == str(self.child.pid) else False
|
return pat.pattern == str(self.child.pid)
|
||||||
if field == 'title':
|
if field == 'title':
|
||||||
return pat.search(self.override_title or self.title) is not None
|
return pat.search(self.override_title or self.title) is not None
|
||||||
if field in 'cwd':
|
if field in 'cwd':
|
||||||
@ -635,6 +649,21 @@ class Window:
|
|||||||
return False
|
return False
|
||||||
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:
|
def set_visible_in_layout(self, val: bool) -> None:
|
||||||
val = bool(val)
|
val = bool(val)
|
||||||
if val is not self.is_visible_in_layout:
|
if val is not self.is_visible_in_layout:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user