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?
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.