]> scripts.mit.edu Git - wizard.git/blobdiff - wizard/prompt.py
Implement interactive mode without validation.
[wizard.git] / wizard / prompt.py
diff --git a/wizard/prompt.py b/wizard/prompt.py
new file mode 100644 (file)
index 0000000..43329e7
--- /dev/null
@@ -0,0 +1,196 @@
+"""
+Interface compatible with :class:`dialog.Dialog` for doing
+non-ncurses interaction.
+
+By convention, the last line of a text parameter should be
+a short value with a trailing colon so that we can prompt a user
+for a value immediately after it.
+"""
+
+import sys
+import readline
+import decorator
+import textwrap
+import getpass
+
+import wizard
+
+try:
+    import dialog
+    has_dialog = True
+except ImportError:
+    has_dialog = False
+
+def fill(text, width=60):
+    return "\n\n".join(textwrap.fill(p, width=width) for p in text.split("\n\n"))
+
+def guess_dimensions(text, width=60):
+    # +1 for the fact that there's no trailing newline from fill
+    # +2 for the borders
+    # +1 as a buffer in case we underestimate
+    return width, fill(text).count("\n", width-2) + 1 + 2 + 1
+
+def make(prompt, non_interactive):
+    if non_interactive:
+        return FailPrompt()
+    if prompt or not has_dialog:
+        return Prompt()
+    try:
+        return Dialog()
+    except dialog.ExecutableNotFound:
+        return Prompt()
+
+def join_or(items):
+    if len(items) == 0:
+        raise ValueError
+    elif len(items) == 1:
+        return items[0]
+    return ', '.join(items[:-1]) + ' or ' + items[-1]
+
+@decorator.decorator
+def dialog_wrap(f, self, text, *args, **kwargs):
+    if 'cmdopt' in kwargs: del kwargs['cmdopt']
+    if 'width' not in kwargs and 'height' not in kwargs:
+        kwargs["width"], kwargs["height"] = guess_dimensions(text)
+    result = f(self, text, *args, **kwargs)
+    if not isinstance(result, tuple):
+        exit = result
+        value = None
+    else:
+        exit, value = result
+    if exit == self.dialog.DIALOG_CANCEL or exit == self.dialog.DIALOG_ESC:
+        raise UserCancel
+    elif exit != self.dialog.DIALOG_OK:
+        # XXX: We don't support stuff like DIALOG_EXTRA or DIALOG_HELP
+        raise DialogError(exit)
+    return value
+
+class Dialog(object):
+    interactive = True
+    """Ncurses interface using dialog."""
+    def __init__(self):
+        self.dialog = dialog.Dialog()
+    @dialog_wrap
+    def inputbox(self, *args, **kwargs):
+        kwargs.setdefault('initerror', "You cannot edit initial value; please type characters after it.")
+        initerror = kwargs['initerror']
+        del kwargs['initerror']
+        kwargs['height'] += 5 # for the text box
+        exit, result = self.dialog.inputbox(*args, **kwargs)
+        if exit == self.dialog.DIALOG_OK:
+            # do some funny munging
+            kwargs.setdefault('init', '')
+            if result[0:len(kwargs['init'])] != kwargs['init']:
+                self.msgbox(initerror, height=10, width=50)
+                exit = self.dialog.DIALOG_OK
+                result = self.inputbox(*args, initerror=initerror, **kwargs)
+            else:
+                result = result[len(kwargs['init']):]
+        return (exit, result)
+    @dialog_wrap
+    def menu(self, *args, **kwargs):
+        kwargs['height'] += 6 + len(kwargs['choices']) # for the border and menu entries
+        return self.dialog.menu(*args, **kwargs)
+    @dialog_wrap
+    def msgbox(self, *args, **kwargs):
+        kwargs['height'] += 3
+        return self.dialog.msgbox(*args, **kwargs)
+    @dialog_wrap
+    def passwordbox(self, *args, **kwargs):
+        kwargs['height'] += 6
+        return self.dialog.passwordbox(*args, **kwargs)
+    @dialog_wrap
+    def infobox(self, text, **kwargs):
+        return self.dialog.infobox(text, **kwargs)
+
+@decorator.decorator
+def prompt_wrap(f, self, *args, **kwargs):
+    try:
+        return f(self, *args, **kwargs)
+    except (EOFError, KeyboardInterrupt):
+        raise UserCancel
+
+class Prompt(object):
+    interactive = True
+    @prompt_wrap
+    def inputbox(self, text, init='', **kwargs):
+        print ""
+        return raw_input(fill(text.strip()) + " " + init)
+    @prompt_wrap
+    def menu(self, text, choices=[], **kwargs):
+        print ""
+        print fill(text.strip())
+        values = list(choice[0] for choice in choices)
+        for choice in choices:
+            print "% 4s  %s" % (choice[0], choice[1])
+        while 1:
+            out = raw_input("Please enter %s: " % join_or(values))
+            if out not in values:
+                print "'%s' is not a valid value" % out
+                continue
+            return out
+    @prompt_wrap
+    def passwordbox(self, text, **kwargs):
+        print ""
+        return getpass.getpass(text + " ")
+    @prompt_wrap
+    def msgbox(self, text, **kwargs):
+        print ""
+        print fill(text.strip())
+        print "Press <Enter> to continue..."
+    @prompt_wrap
+    def infobox(self, text, **kwargs):
+        print ""
+        print fill(text.strip())
+
+class FailPrompt(object):
+    """
+    Prompt that doesn't actually ask the user; just fails with
+    an error message.
+    """
+    interactive = False
+    def inputbox(self, *args, **kwargs):
+        kwargs.setdefault('cmdopt', '(unknown)')
+        raise MissingRequiredParam(kwargs['cmdopt'])
+    def passwordbox(self, *args, **kwargs):
+        kwargs.setdefault('cmdopt', '(unknown)')
+        raise MissingRequiredParam(kwargs['cmdopt'])
+    def menu(self, *args, **kwargs):
+        kwargs.setdefault('cmdopt', '(unknown)')
+        raise MissingRequiredParam(kwargs['cmdopt'])
+    def msgbox(self, text, **kwargs):
+        print ""
+        print fill(text.strip())
+    def infobox(self, text, **kwargs):
+        print ""
+        print fill(text.strip())
+
+class Error(wizard.Error):
+    pass
+
+class MissingRequiredParam(Error):
+    """Non-interactive, but we needed more info."""
+    def __init__(self, cmdopt):
+        self.cmdopt = cmdopt
+    def __str__(self):
+        return """
+
+ERROR: Missing required parameter, try specifying %s""" % self.cmdopt
+
+class UserCancel(Error):
+    """User canceled the input process."""
+    def __str__(self):
+        return "\nAborting installation process; no changes were made"
+
+class DialogError(Error):
+    """Dialog returned a mysterious error."""
+    def __init__(self, exit):
+        self.exitcode = exit
+    def __str__(self):
+        return """
+
+ERROR:  Dialog returned a mysterious exit code %d.  Please
+send mail to scripts@mit.edu with the contents of this error
+message and the preceding backtrace.
+""" % self.exitcode
+