Search code examples
sublimetext3sublimetext2sublimetextsublime-text-plugin

Prompt user for a password and hide the input with Sublime Text


I'd like to have a prompt that queries a password inside a Sublime Text 2/3 plugin and hide the input with ****. This does not work and even crash SublimeText:

import sublime_plugin, sublime

class ExampleOneCommand(sublime_plugin.WindowCommand):
    def run(self):
        self.window.show_input_panel("Enter Password", "", self.on_done, self.getpwd, None)

    def getpwd(self, password):
        stars = "*" * len(password)
        self.window.show_input_panel("Enter Password", stars, self.on_input, self.getpwd, None)

    def on_done(self, password):
        pass

    def on_input(self, password):
        pass

How to do it properly?


Solution

  • As written above, when I invoke your command it crashes the plugin host with a RuntimeError of maximum recursion depth reached. The reason why this is happening is perhaps best illustrated with this simple example:

    class ExampleTwoCommand(sublime_plugin.WindowCommand):
        def run(self):
            self.window.show_input_panel("Sample", "Initial Text", None, self.on_change, None)
    
        def on_change(self, pwd):
            print('on_change("%s")' % pwd)
    

    If you invoke the command and then immediately look in the Console without interacting with the input, you see this:

    >>> window.run_command("example_two")
    on_change("Initial Text")
    

    So essentially your problem is that when the input panel opens, it tells the on_change() handler about the initial text, and your command responds to that by immediately opening the input panel again, which does the same thing and then kaboom.

    To get around that you need to have a guard in place that stops this from happening; for example:

    class ExampleThreeCommand(sublime_plugin.WindowCommand):
        def run(self):
            self.initial = ""
            self.window.show_input_panel("Sample", "", None, self.on_change, None)
    
        def on_change(self, pwd):
            if self.initial != pwd:
                stars = "*" * len(pwd)
                self.initial = pwd
                self.window.show_input_panel("Sample", stars, None, self.on_change, None)
    

    Now the on_change() handler will only re-open the input handler if the text changes.

    A consequence of this is that the text that ultimately gets returned to you is the initial text with your edits, so when on_done is finally called, the text that it gets is going to contain all * characters due to the manipulations here.

    To get around that you would need to check the password given in on_change to see what characters it contains that are not * characters to know what was actually typed, and then keep that separate so that you keep the text as the user is editing it. Similarly if the length of the password changes you need to throw characters that you've saved away because someone has removed a character or characters from the input. You would also need to take care of the case of the user moving the cursor inside the panel as well (such as inserting characters).

    With all of that said, the simplest example of something that does all of this is the following, which uses the password setting to tell the input panel that no matter what is typed, it should be obscured:

    class ExampleFourCommand(sublime_plugin.WindowCommand):
        def run(self):
            panel = self.window.show_input_panel("Enter Password", "", self.on_done, None, None)
            panel.settings().set("password", True)
    
        def on_done(self, password):
            sublime.message_dialog("Password entered: '%s'" % password)
    

    I'm not sure if this setting is supported in Sublime Text 2 though; it's used in Sublime Merge's password input when you're entering passwords there, so possibly it was added around then.

    If Sublime Text 2 support is required, the next best thing would be something like this combination of plugin and an associated key binding (which I think should work there, but I don't have a copy to test with):

    _password = ""
    class ExampleFiveCommand(sublime_plugin.WindowCommand):
        def run(self):
            panel = self.window.show_input_panel("Enter Password", "", self.on_done, None, None)
            panel.settings().set("password_input", True)
    
            global _password
            _password = ""
    
        def on_done(self, password):
            sublime.message_dialog("Password entered: '%s'" % _password)
    
    
    class PasswordInputCommand(sublime_plugin.TextCommand):
        def run(self, edit, character):
            global _password
            _password += character
            self.view.run_command("insert", {"characters": '*'})
    
        { "keys": ["<character>"], "command": "password_input", "context": [
            { "key": "setting.password_input", "operator": "equal", "operand": true },
        ],
        },
    

    Here the key binding executes a custom command when you enter a character, but only when the view has the setting password_input and its set to true. The associated commands keep track of the actual password in a string and opening the input panel applies the setting. The result is that as long as the panel is open and you're typing in it, the custom command grabs the text and keeps it.

    For a more complete example you would also need to bind the backspace key in case the user wants to do corrections. You can also use the selection in the view in order to determine where in the existing password the character pressed should go.

    On the whole, the password setting is the cleanest solution, though.