Allow matching on user vars

This commit is contained in:
Kovid Goyal
2023-07-27 16:54:50 +05:30
parent 39bd1a2d57
commit 2dfad8e854
4 changed files with 29 additions and 28 deletions

View File

@@ -480,7 +480,7 @@ class Boss:
return {wid for wid in candidates if self.window_id_map[wid].matches_query(location, query, tab, self_window)}
for wid in search(match, (
'id', 'title', 'pid', 'cwd', 'cmdline', 'num', 'env', 'recent', 'state'
'id', 'title', 'pid', 'cwd', 'cmdline', 'num', 'env', 'var', 'recent', 'state'
), set(self.window_id_map), get_matches):
yield self.window_id_map[wid]
@@ -516,7 +516,7 @@ class Boss:
found = False
for tid in search(match, (
'id', 'index', 'title', 'window_id', 'window_title', 'pid', 'cwd', 'env', 'cmdline', 'recent', 'state'
'id', 'index', 'title', 'window_id', 'window_title', 'pid', 'cwd', 'env', 'var', 'cmdline', 'recent', 'state'
), set(tim), get_matches):
found = True
yield tim[tid]

View File

@@ -83,7 +83,7 @@ MATCH_WINDOW_OPTION = '''\
--match -m
The window to match. Match specifications are of the form: :italic:`field:query`.
Where :italic:`field` can be one of: :code:`id`, :code:`title`, :code:`pid`, :code:`cwd`, :code:`cmdline`, :code:`num`,
:code:`env`, :code:`state` and :code:`recent`.
:code:`env`, :code:`var`, :code:`state`, and :code:`recent`.
:italic:`query` is the expression to match. Expressions can be either a number or a regular expression, and can be
:ref:`combined using Boolean operators <search_syntax>`.
@@ -104,6 +104,9 @@ active window, one being the previously active window and so on.
When using the :code:`env` field to match on environment variables, you can specify only the environment variable name
or a name and value, for example, :code:`env:MY_ENV_VAR=2`.
Similarly, the :code:`var` field matches on user variables set on the window. You can specify name or name and value
as with the :code:`env` field.
The field :code:`state` matches on the state of the window. Supported states
are: :code:`active`, :code:`focused`, :code:`needs_attention`,
:code:`parent_active`, :code:`parent_focused`, :code:`self`,
@@ -121,7 +124,7 @@ MATCH_TAB_OPTION = '''\
--match -m
The tab to match. Match specifications are of the form: :italic:`field:query`.
Where :italic:`field` can be one of: :code:`id`, :code:`index`, :code:`title`, :code:`window_id`, :code:`window_title`,
:code:`pid`, :code:`cwd`, :code:`cmdline` :code:`env`, :code:`state` and :code:`recent`.
:code:`pid`, :code:`cwd`, :code:`cmdline` :code:`env`, :code:`var`, :code:`state` and :code:`recent`.
:italic:`query` is the expression to match. Expressions can be either a number or a regular expression, and can be
:ref:`combined using Boolean operators <search_syntax>`.
@@ -143,7 +146,7 @@ active tab, one the previously active tab and so on.
When using the :code:`env` field to match on environment variables, you can specify only the environment variable name
or a name and value, for example, :code:`env:MY_ENV_VAR=2`. Tabs containing any window with the specified environment
variables are matched.
variables are matched. Similarly, :code:`var` matches tabs containing any window with the specified user variable.
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`.

View File

@@ -1203,3 +1203,11 @@ def get_custom_window_icon() -> Union[Tuple[float, str], Tuple[None, None]]:
if custom_icon_mtime is not None:
return custom_icon_mtime, custom_icon_path
return None, None
def key_val_matcher(items: Iterable[Tuple[str, str]], key_pat: re.Pattern[str], val_pat: Optional[re.Pattern[str]]) -> bool:
for key, val in items:
if key_pat.search(key) is not None and (
val_pat is None or val_pat.search(val) is not None):
return True
return False

View File

@@ -91,6 +91,7 @@ from .types import MouseEvent, OverlayType, WindowGeometry, ac, run_once
from .typing import BossType, ChildType, EdgeLiteral, TabType, TypedDict
from .utils import (
docs_url,
key_val_matcher,
kitty_ansi_sanitizer_pat,
log_error,
open_cmd,
@@ -204,7 +205,7 @@ class WindowDict(TypedDict):
is_self: bool
lines: int
columns: int
user_vars: Dict[str, Union[str, Dict[str, str]]]
user_vars: Dict[str, str]
class PipeData(TypedDict):
@@ -523,7 +524,7 @@ class Window:
self.default_title = os.path.basename(child.argv[0] or appname)
self.child_title = self.default_title
self.title_stack: Deque[str] = deque(maxlen=10)
self.user_vars: Dict[str, bytes] = {}
self.user_vars: Dict[str, str] = {}
self.id: int = add_window(tab.os_window_id, tab.id, self.title)
self.clipboard_request_manager = ClipboardRequestManager(self.id)
self.margin = EdgeWidths()
@@ -635,15 +636,6 @@ class Window:
def __repr__(self) -> str:
return f'Window(title={self.title}, id={self.id})'
@property
def serializeable_user_vars(self) -> Dict[str, Union[str, Dict[str, str]]]:
from base64 import standard_b64encode
def s(x: bytes) -> Union[str, Dict[str, str]]:
with suppress(UnicodeDecodeError):
return x.decode('utf-8')
return {'value': standard_b64encode(x).decode('ascii'), 'encoding': 'base64'}
return {k: s(v) for k, v in self.user_vars.items()}
def as_dict(self, is_focused: bool = False, is_self: bool = False, is_active: bool = False) -> WindowDict:
return {
'id': self.id,
@@ -658,7 +650,7 @@ class Window:
'is_self': is_self,
'lines': self.screen.lines,
'columns': self.screen.columns,
'user_vars': self.serializeable_user_vars,
'user_vars': self.user_vars,
}
def serialize_state(self) -> Dict[str, Any]:
@@ -682,7 +674,7 @@ class Window:
if self.overlay_type is not OverlayType.transient:
ans['overlay_type'] = self.overlay_type.value
if self.user_vars:
ans['user_vars'] = self.serializeable_user_vars
ans['user_vars'] = self.user_vars
return ans
@property
@@ -707,15 +699,13 @@ class Window:
def matches(self, field: str, pat: MatchPatternType) -> bool:
if not pat:
return False
if field == 'env':
assert isinstance(pat, tuple)
key_pat, val_pat = pat
for key, val in self.child.environ.items():
if key_pat.search(key) is not None and (
val_pat is None or val_pat.search(val) is not None):
return True
if isinstance(pat, tuple):
if field == 'env':
return key_val_matcher(self.child.environ.items(), *pat)
if field == 'var':
return key_val_matcher(self.user_vars.items(), *pat)
return False
assert not isinstance(pat, tuple)
if field in ('id', 'window_id'):
return pat.pattern == str(self.id)
@@ -765,7 +755,7 @@ class Window:
if query == 'overlay_parent':
return self_window is not None and self is self_window.overlay_parent
return False
pat = compile_match_query(query, field != 'env')
pat = compile_match_query(query, field not in ('env', 'var'))
return self.matches(field, pat)
def set_visible_in_layout(self, val: bool) -> None:
@@ -866,7 +856,7 @@ class Window:
oldest_key = next(iter(self.user_vars))
self.user_vars.pop(oldest_key)
if val is not None:
self.user_vars[key] = val
self.user_vars[key] = val.decode('utf-8', 'replace')
# screen callbacks {{{