pythonlinuxfile-manager

Take user inputs in ranger file manager


I want to create a custom command for ranger, however, I have no Idea about how to get an user inputs since it freezes when I use the traditional way input(). I already spent hours googling it but nothing worthy so far.

class user_input(Command):
    def execute(self):
        file = self.fm.ui.console.input('user input...') # '.input()' doesn't exist
        self.fm.notify(file)

I'd really appreciate the help.


Solution

  • TL;DR

    Use self.arg(1).

    Background

    The ranger file manager provides a command system wherein users can issue a command by typing : followed by the command's name along with any arguments to that command. A simple example is the search command, which you can use by typing :search [your search term here].

    Ranger is extensible with the Python programming language. One can write custom keybindings, commands, and even plugins for ranger in Python. I explain the process further in an aside at the end of this post.

    Our focus here is that one can define a custom command in the form of a Python class. This class must inherit from ranger's Command class and supply an entry point method called execute. Ranger will call this method when the command is invoked. The name of the derived class is the name that you type in ranger's console to invoke the command.

    Argument Handling

    Input is supplied to a command in the form of arguments. To write a command that takes input, simply use the arguments passed to that command. Let's consider a minimal working example. You can append the following code snippet to the commands.py file located in the ~/.config/ranger directory.

    from ranger.api.commands import Command
    
    class myCommand(Command):
    
        def execute(self):
    
            filename = self.arg(1)   # <--- This is your answer.
    
            self.fm.notify(filename) # in lieu of something more interesting
    

    Understand that input is passed into a command manually by the user as part of that command's invocation. To demonstrate, in ranger you would type :myCommand flowers.jpg and then press enter. Here myCommand is the command and flowers.jpg is its argument. It is all done in one go. Hence, there is no need to prompt the user for input inside your execute method because ranger already takes care of input for you from the outside. All you have to know is how to access your command's arguments.

    The methods and attributes of the Command class are described in ranger's source code file commands.py. Among them are ones pertaining to argument handling. Use self.arg(n) and friends to access a command's input arguments.

    • self.line The whole line that was written in the console.
    • self.args A list of all (space-separated) arguments to the command.
    • self.quantifier If this command was mapped to the key "x" and the user pressed 6x, self.quantifier would be 6.
    • self.arg(n) The n-th argument, or an empty string if it doesn't exist.
    • self.rest(n) The n-th argument plus everything that followed. For example, if the command was "search foo bar a b c", rest(2) would be "bar a b c".
    • self.start(n) Anything before the n-th argument. For example, if the command was "search foo bar a b c", start(2) will be "search foo".

    Consider looking at the implementation of ranger's echo command in the file linked just above.

    Based on your question, I presume that you would prefer something more interactive analogous to Python's built-in input() function. If that is so, then consider the following alternative.

    Prompted Input

    One limitation of the above solution is that the user's input is tethered to the entry point of the command. It would be nice if there was a distinct user_input function that could be called with a custom prompt multiple times throughout a body of code independent of a command's invocation. As far as I know, ranger does not provide anything like this, but we can write it ourselves. Therefore, let's consider a more sophisticated solution.

    The ranger file manager uses the curses programming library to handle input and much else. You can write your own user_input function like this. I have also included an example of how to use the function.

    from ranger.api.commands import Command
    import curses
    
    class greet(Command):
    
        def execute(self):
    
            greeting = "Hello, {person}!"
    
            name = user_input("Please enter your name. ")
    
            # You can call user_input as many times as you'd like.
    
            name = user_input("What? ")
    
            self.fm.notify(greeting.format(person=name))
    
    def user_input(prompt):
    
        """
        Prompt the user for input. For use with the ranger file manager.
    
        :param str prompt: The prompt to the user
        :return: The user's input
        :rtype: str
        """
    
        # Tested with
    
            # ranger version 1.9.3
            # python version 3.10.12
    
        # start a curses window
        window = curses.initscr()
    
        # get the coordinates of the farthest row and column, subtracting one
        rows, cols = [coord - 1 for coord in window.getmaxyx()]
    
        # add a prompt in ranger's status bar
        window.addstr(rows, 0, prompt)
    
        # enable the echoing of entered characters
        curses.echo()
    
        # get and display user input after the prompt
        user_input_bytes = window.getstr(rows, len(prompt), cols)
    
        # disable the echoing of entered characters
        curses.noecho()
    
        # clear ranger's status bar for the next use
        window.addstr(rows, 0, " " * cols)
    
        # end the window
        curses.endwin()
    
        # return the user input as a string
        return user_input_bytes.decode(encoding="utf-8")
    

    Once you have saved the above code in ~/.config/ranger/plugins/plugin_greeter.py, you can run it by opening ranger at the command line and typing :greet followed by the enter key. You will then be prompted for a string in the bottom left-hand corner of the screen.

    It is possible to achieve more intricate functionality. Instead of prompting the user for a string, imagine that you want to implement your own custom keybindings, particularly something that you couldn't otherwise do with ranger's mapping facilities. You can use window.getch() for that. I recently did this in my own ranger plugin.

    An Aside

    Ranger provides a plugin system. A plugin is typically a single file conventionally called plugin_[name].py. They are stored in the ~/.config/ranger/plugins directory. Ranger's GitHub repository contains several examples. To learn more, read the PLUGINS section of ranger's man page. The --debug flag is also helpful when writing plugins. Smaller, standalone commands can be put in ~/.config/ranger/commands.py and mapped to in ~/.config/ranger/rc.conf as described here. Create a plugin when you have a longer command or several interrelated commands, keybindings, etc. In general, writing a plugin is a good way to isolate your own code from ranger's code, since commands.py is already populated with built-ins.