From 2dfad8e854bc6071e48a8f7f5414e7435430e704 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Jul 2023 16:54:50 +0530 Subject: [PATCH] Allow matching on user vars --- kitty/boss.py | 4 ++-- kitty/rc/base.py | 9 ++++++--- kitty/utils.py | 8 ++++++++ kitty/window.py | 36 +++++++++++++----------------------- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/kitty/boss.py b/kitty/boss.py index a173f2fdf..7103df35b 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -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] diff --git a/kitty/rc/base.py b/kitty/rc/base.py index 3fcc11a02..b0b5bc5cd 100644 --- a/kitty/rc/base.py +++ b/kitty/rc/base.py @@ -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 `. @@ -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 `. @@ -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`. diff --git a/kitty/utils.py b/kitty/utils.py index 31688e8d8..a0f4ec056 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -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 diff --git a/kitty/window.py b/kitty/window.py index fe1323bd5..3e1d70c90 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -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 {{{