With the help of ChatGPT I constructed this script to search through files with ripgrep, fzf, bat.

I have them installed first:

sudo pacman -Syu ripgrep fzf bat

The fzf_search_text command is to be added to ~/.config/ranger/commands.py.


fzf_search_text Command

class fzf_search_text(Command):
    """
    :fzf_search_text
    Search within text files for a given pattern using ripgrep (rg).
    Results are displayed in fzf with previews.
    If no argument is provided, prompts the user for input.
    """

    def execute(self):
        if not self.arg(1):
            # Prompt the user for a search string
            self.fm.open_console("fzf_search_text ")
            return

        search_string = self.rest(1)

        import subprocess
        import os.path
        from ranger.container.file import File

        # Ripgrep command to search for the pattern
        rg_command = (
            f"rg --column --line-number --hidden --no-heading --color=always '{search_string}' . "
            "| fzf --ansi --preview "
            "'echo {} | cut -d: -f1 | xargs bat --color=always --style=numbers || cat {}'"
        )

        # Run the combined ripgrep + fzf command
        fzf = self.fm.execute_command(rg_command, universal_newlines=True, stdout=subprocess.PIPE)
        stdout, _ = fzf.communicate()

        # If a result is selected, process it
        if fzf.returncode == 0:
            result = stdout.strip()
            parts = result.split(":", 1)  # Split at the first colon to get the file path
            if len(parts) >= 1:
                file_path = os.path.abspath(parts[0])  # Extract the file path
                self.fm.select_file(file_path)  # Select the file in ranger
            else:
                self.fm.notify("Unexpected result format. Ensure the file exists.", bad=True)

How It Works

  1. No Argument Case:

    • When invoked without arguments (e.g., by pressing F4), the command opens ranger’s console with fzf_search_text pre-filled, allowing you to type the search string interactively.
  2. With Argument:

    • If you provide an argument (e.g., :fzf_search_text TODO), it directly uses the argument as the search string.

Keybinding in rc.conf

To map this command to F4, add the following line to your ~/.config/ranger/rc.conf:

map F4 fzf_search_text

Usage

  1. Restart ranger.
  2. Press F4 to open a search prompt.
  3. Enter your search string and press Enter.
  4. Results will display in fzf with previews, and you can select a file to navigate to it.

This approach ensures compatibility with ranger’s console and provides a seamless user experience.


It works wonders! I’m very happy with the result and the UX I’ve tuned. I press F4, enter the string that I need, then I can scroll through the files containing the text. It works similar to fzf_select


Upgrading fzf_content_open

Turned out, this script works better, except it opens the file immediately. But maybe it’s just more useful.

https://github.com/ranger/ranger/wiki/Custom-Commands#search-file-content-in-current-directory-recursively-with-fzf-and-open-target-with-neovim

But I don’t like that it says ‘Failed to suspend UI’ when I close my nvim session. Can you help me fix that?

The issue occurs because ranger tries to manage its UI but cannot properly suspend and resume when external commands like nvim take over. To fix this, we need to ensure ranger’s UI is suspended before running nvim and resumed correctly afterward. Ranger provides the fm.execute_command method to handle external commands gracefully, but we must configure it correctly.

Here’s the updated and improved version of your fzf_content_open command:

Fixed fzf_content_open

The issue arises because the subprocess module is not explicitly imported in the script. Let’s fix this by adding the necessary import subprocess statement. Here’s the corrected script:

Corrected fzf_content_open

class fzf_content_open(Command):
    """
    :fzf_content_open
    Pre-requisites: fzf, rg, bat, awk, vim or neovim.
    Uses `rg` to search file content recursively in the current directory.
    Filters with `fzf` and previews with `bat`.
    Pressing `Enter` on a result opens it in (neo)vim at the matched line.
    """

    def execute(self):
        import os
        import subprocess  # Add the missing import
        from ranger.ext.get_executables import get_executables

        # Check required executables
        rg = 'rg' if 'rg' in get_executables() else None
        fzf = 'fzf' if 'fzf' in get_executables() else None
        bat = 'bat' if 'bat' in get_executables() else None
        editor = 'nvim' if 'nvim' in get_executables() else 'vim' if 'vim' in get_executables() else None

        if not all([rg, fzf, bat, editor]):
            self.fm.notify("Missing required executables: rg, fzf, bat, and/or a suitable editor.", bad=True)
            return

        # Prevent recursive search from the home directory
        if self.fm.thisdir.path == self.fm.home_path:
            self.fm.notify("Searching from the home directory is not allowed.", bad=True)
            return

        # Run the combined rg + fzf + bat command
        command = (
            "rg --line-number --hidden . | "
            "fzf --delimiter ':' "
            "--preview 'bat --color=always --highlight-line {2} {1}' "
            "| awk -F ':' '{print \"+\"$2\" \"$1}'"
        )
        process = self.fm.execute_command(command, universal_newlines=True, stdout=subprocess.PIPE)
        stdout, _ = process.communicate()

        # If a result is selected, process it
        if process.returncode == 0:
            if len(stdout) < 2:
                return

            selected_line = stdout.split()[0]  # Line number
            file_path = stdout.split()[1].strip()  # File path

            # Get the absolute path of the file
            file_fullpath = os.path.abspath(file_path)

            # Open the file in the editor at the selected line
            editor_command = [editor, selected_line, file_fullpath]
            self.fm.execute_command(editor_command)

Key Changes:

  1. Added import subprocess:
    • The missing import ensures that the subprocess module is available for use in the script.

Reminder:

  • Add the hotkey mapping in ~/.config/ranger/rc.conf if you haven’t already:
map F5 fzf_content_open

It works better than the original, but I have no understanding of Python to understand if it worsened the original script or improved it. It has no previous error, so I’m going to use till it’s proven itself otherwise.