diff --git a/docs/changelog.rst b/docs/changelog.rst index beb02c1a2..6d1f42054 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,6 +14,9 @@ To update |kitty|, :doc:`follow the instructions `. - Add support for easily editing or downloading files over SSH sessions without the need for any special software, see :doc:`kittens/remote_file` +- A new :doc:`kittens/hyperlinked_grep` kitten to easily search files and open + the results at the matched line by clicking on them. + - Improve rendering of borders when using minimal borders. Use less space and do not display a box around active windows diff --git a/docs/kittens/hyperlinked_grep.rst b/docs/kittens/hyperlinked_grep.rst new file mode 100644 index 000000000..81f1ef0ad --- /dev/null +++ b/docs/kittens/hyperlinked_grep.rst @@ -0,0 +1,43 @@ +Hyperlinked grep +================= + +This kitten allows you to search your files using `ripgrep +`_ and open the results +directly in your favorite editor in the terminal, at the line containing +the search result, simply by clicking on the result you want. + +To set it up, first create :file:`~/.config/kitty/open-actions.conf` with the +following contents: + +.. code:: conf + + # Open any file with a fragment in the editor, fragments are generated + # by the hyperlink_grep kitten and nothing else so far. + protocol file + has_fragment yes + action launch --type=overlay vim +$FRAGMENT $FILE_PATH + + # Open text files without fragments in the editor + protocol file + mime text/* + has_fragment no + action launch --type=overlay $EDITOR $FILE_PATH + + +Now, run a search with:: + + kitty +kitten hyperlinked_grep something + +Hold down the :kbd:`ctrl+shift` keys and click on any of the +result lines, to open the file in vim at the matching line. If +you use some editor other than vim, you should adjust the +:file:`open-actions.conf` file accordingly. + +Finally, add an alias to your shell's rc files to invoke the kitten as ``hg``:: + + alias kitty +kitten hyperlink_grep + + +You can now run searches with:: + + hg some-search-term diff --git a/kittens/hyperlinked_grep/__init__.py b/kittens/hyperlinked_grep/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kittens/hyperlinked_grep/main.py b/kittens/hyperlinked_grep/main.py new file mode 100644 index 000000000..45e9149a5 --- /dev/null +++ b/kittens/hyperlinked_grep/main.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2020, Kovid Goyal + +import os +import re +import socket +import subprocess +import sys +from typing import Callable, cast +from urllib.parse import quote_from_bytes + + +def write_hyperlink(write: Callable[[bytes], None], url: bytes, line: bytes, frag: bytes = b'') -> None: + write(b'\033]8;;') + write(url) + if frag: + write(b'#') + write(frag) + write(b'\033\\') + write(line) + write(b'\033]8;;\033\\') + + +def main() -> None: + if not sys.stdout.isatty() and '--pretty' not in sys.argv: + os.execlp('rg', 'rg', *sys.argv[1:]) + cmdline = ['rg', '--pretty'] + sys.argv[1:] + p = subprocess.Popen(cmdline, stdout=subprocess.PIPE) + assert p.stdout is not 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(b'^(\\d+):') + + in_result: bytes = b'' + hostname = socket.gethostname().encode('utf-8') + + for line in p.stdout: + line = osc_pat.sub(b'', line) # remove any existing hyperlinks + clean_line = sgr_pat.sub(b'', line).rstrip() # remove SGR formatting + if not clean_line: + in_result = b'' + write(b'\n') + continue + if in_result: + m = num_pat.match(clean_line) + if m is not None: + write_hyperlink(write, in_result, line, frag=m.group(1)) + 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 __name__ == '__main__': + main()