diff --git a/docs/changelog.rst b/docs/changelog.rst index 28b63ea53..5d3d3d472 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -31,6 +31,9 @@ To update |kitty|, :doc:`follow the instructions `. it possible for programs running on the local machine to control kitty but not programs running over ssh. +- hints kitten: Allow using named groups in the regular expression. The named + groups are passed to the invoked program for further processing. + - Fix a regression in 0.14.5 that caused rendering of private use glyphs with and without spaces to be identical (:iss:`2117`) diff --git a/kittens/hints/main.py b/kittens/hints/main.py index 09f51594e..b1d57fb11 100644 --- a/kittens/hints/main.py +++ b/kittens/hints/main.py @@ -26,11 +26,12 @@ screen_size = screen_size_function() class Mark: - __slots__ = ('index', 'start', 'end', 'text') + __slots__ = ('index', 'start', 'end', 'text', 'groupdict') - def __init__(self, index, start, end, text): + def __init__(self, index, start, end, text, groupdict): self.index, self.start, self.end = index, start, end self.text = text + self.groupdict = groupdict @lru_cache(maxsize=2048) @@ -94,6 +95,14 @@ class Hints(Handler): self.chosen = [] self.reset() + @property + def text_matches(self): + return [m.text + self.match_suffix for m in self.chosen] + + @property + def groupdicts(self): + return [m.groupdict for m in self.chosen] + def get_match_suffix(self, args): if args.add_trailing_space == 'always': return ' ' @@ -126,7 +135,7 @@ class Hints(Handler): if encode_hint(idx, self.alphabet).startswith(self.current_input) ] if len(matches) == 1: - self.chosen.append(matches[0].text + self.match_suffix) + self.chosen.append(matches[0]) if self.multiple: self.ignore_mark_indices.add(matches[0].index) self.reset() @@ -144,7 +153,7 @@ class Hints(Handler): elif key_event is enter_key and self.current_input: try: idx = decode_hint(self.current_input, self.alphabet) - self.chosen.append(self.index_map[idx].text + self.match_suffix) + self.chosen.append(self.index_map[idx]) self.ignore_mark_indices.add(idx) except Exception: self.current_input = '' @@ -176,12 +185,13 @@ class Hints(Handler): def regex_finditer(pat, minimum_match_length, text): + has_named_groups = bool(pat.groupindex) for m in pat.finditer(text): - s, e = m.span(pat.groups) + s, e = m.span(0 if has_named_groups else pat.groups) while e > s + 1 and text[e-1] == '\0': e -= 1 if e - s >= minimum_match_length: - yield s, e + yield s, e, m.groupdict() closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'"} @@ -240,11 +250,11 @@ def quotes(text, s, e): def mark(pattern, post_processors, text, args): pat = re.compile(pattern) - for idx, (s, e) in enumerate(regex_finditer(pat, args.minimum_match_length, text)): + for idx, (s, e, groupdict) in enumerate(regex_finditer(pat, args.minimum_match_length, text)): for func in post_processors: s, e = func(text, s, e) mark_text = text[s:e].replace('\n', '').replace('\0', '') - yield Mark(idx, s, e, mark_text) + yield Mark(idx, s, e, mark_text, groupdict) def run_loop(args, text, all_marks, index_map): @@ -252,9 +262,9 @@ def run_loop(args, text, all_marks, index_map): handler = Hints(text, all_marks, index_map, args) loop.loop(handler) if handler.chosen and loop.return_code == 0: - return {'match': handler.chosen, 'programs': args.program, + return {'match': handler.text_matches, 'programs': args.program, 'multiple_joiner': args.multiple_joiner, - 'type': args.type} + 'type': args.type, 'groupdicts': handler.groupdicts} raise SystemExit(loop.return_code) @@ -356,11 +366,14 @@ The type of text to search for. --regex default=(?m)^\s*(.+)\s*$ The regular expression to use when :option:`kitty +kitten hints --type`=regex. -The regular expression is in python syntax. If you specify a group in +The regular expression is in python syntax. If you specify a numbered group in the regular expression only the group will be matched. This allow you to match text ignoring a prefix/suffix, as needed. The default expression matches lines. To match text over multiple lines you should prefix the regular expression with :code:`(?ms)`, which turns on MULTILINE and DOTALL modes for the regex engine. +If you specify named groups and a :option:`kitty +kitten hints --program` then +the program will be passed arguments corresponding to each named group of +the form key=value. --url-prefixes @@ -452,7 +465,10 @@ def main(args): def handle_result(args, data, target_window_id, boss): programs = data['programs'] or ('default',) - matches = tuple(filter(None, data['match'])) + matches, groupdicts = [], [] + for m, g in zip(data['match'], data['groupdicts']): + if m: + matches.append(m), groupdicts.append(g) joiner = data['multiple_joiner'] try: is_int = int(joiner) @@ -489,7 +505,11 @@ def handle_result(args, data, target_window_id, boss): if w is not None: cwd = w.cwd_of_child program = None if program == 'default' else program - for m in matches: + for m, groupdict in zip(matches, groupdicts): + if groupdict: + m = [] + for k, v in groupdict.items(): + m.append('{}={}'.format(k, v or '')) boss.open_url(m, program, cwd=cwd) diff --git a/kitty/utils.py b/kitty/utils.py index c6d8ddaba..e0f1be9fc 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -164,7 +164,10 @@ def open_cmd(cmd, arg=None, cwd=None): import subprocess if arg is not None: cmd = list(cmd) - cmd.append(arg) + if isinstance(arg, (list, tuple)): + cmd.extend(arg) + else: + cmd.append(arg) return subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cwd or None)