diff --git a/docs/kittens/hyperlinked_grep.rst b/docs/kittens/hyperlinked_grep.rst index eba86c143..8287de93d 100644 --- a/docs/kittens/hyperlinked_grep.rst +++ b/docs/kittens/hyperlinked_grep.rst @@ -74,6 +74,14 @@ to :program:`rg`. How to do that varies based on the shell: To learn more about kitty's powerful framework for customizing URL click actions, see :doc:`here `. +By default, this adds hyperlinks for several parts of ripgrep output: the +per-file header, match context lines, and match lines. You can control which +items are linked with a :command:`--kitten hyperlink` flag. For example, +:command:`--kitten hyperlink=matching_lines` will only add hyperlinks to the +match lines. :command:`--kitten hyperlink=file_headers,context_lines` will +link file headers and context lines but not match lines. +:command:`--kitten hyperlink` may be specified multiple times. + Hopefully, someday this functionality will make it into some `upstream grep `__ program directly removing the need for this kitten. diff --git a/kittens/hyperlinked_grep/main.py b/kittens/hyperlinked_grep/main.py old mode 100644 new mode 100755 index 3c84998b4..dc787cae2 --- a/kittens/hyperlinked_grep/main.py +++ b/kittens/hyperlinked_grep/main.py @@ -20,6 +20,29 @@ def write_hyperlink(write: Callable[[bytes], None], url: bytes, line: bytes, fra def main() -> None: + i = 1 + all_link_options = ['matching_lines', 'context_lines', 'file_headers'] + link_options = set() + while i < len(sys.argv): + if sys.argv[i] == '--kitten': + if len(sys.argv) < i + 2 or not sys.argv[i + 1].startswith("hyperlink="): + raise SystemExit("--kitten argument must be followed by hyperlink=(all|matching_lines|context_lines|file_headers)") + for option in sys.argv[i + 1].split('=')[1].split(','): + if option == 'all': + link_options.update(all_link_options) + elif option not in all_link_options: + raise SystemExit(f"hyperlink option must be one of all, matching_lines, context_lines, or file_headers, not '{option}'") + else: + link_options.add(option) + del sys.argv[i:i+2] + else: + i += 1 + if len(link_options) == 0: # Default to linking everything if no options given + link_options.update(all_link_options) + link_file_headers = 'file_headers' in link_options + link_context_lines = 'context_lines' in link_options + link_matching_lines = 'matching_lines' in link_options + if not sys.stdout.isatty() and '--pretty' not in sys.argv and '-p' not in sys.argv: os.execlp('rg', 'rg', *sys.argv[1:]) cmdline = ['rg', '--pretty', '--with-filename'] + sys.argv[1:] @@ -31,7 +54,7 @@ def main() -> None: write: Callable[[bytes], None] = cast(Callable[[bytes], None], sys.stdout.buffer.write) sgr_pat = re.compile(br'\x1b\[.*?m') osc_pat = re.compile(b'\x1b\\].*?\x1b\\\\') - num_pat = re.compile(br'^(\d+)[:-]') + num_pat = re.compile(br'^(\d+)([:-])') in_result: bytes = b'' hostname = socket.gethostname().encode('utf-8') @@ -43,20 +66,22 @@ def main() -> None: if not clean_line: in_result = b'' write(b'\n') - continue - if in_result: + elif in_result: m = num_pat.match(clean_line) if m is not None: - write_hyperlink(write, in_result, line, frag=m.group(1)) - else: - write(line) + is_match_line = m.group(2) == b':' + if (is_match_line and link_matching_lines) or (not is_match_line and link_context_lines): + write_hyperlink(write, in_result, line, frag=m.group(1)) + continue + write(line) else: if line.strip(): path = quote_from_bytes(os.path.abspath(clean_line)).encode('utf-8') in_result = b'file://' + hostname + path - write_hyperlink(write, in_result, line) - else: - write(line) + if link_file_headers: + write_hyperlink(write, in_result, line) + continue + write(line) except KeyboardInterrupt: p.send_signal(signal.SIGINT) except (EOFError, BrokenPipeError):