""" 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 os import wizard try: import dialog has_dialog = True except ImportError: has_dialog = False def fill(text, width=60, **kwargs): return "\n\n".join(textwrap.fill(p, width=width, **kwargs) 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 print fill(text) return width, fill(text, width-2).count("\n") + 1 + 2 + 1 def make(prompt, non_interactive): if non_interactive: return FailPrompt() if prompt or os.getenv('TERM') == 'dumb' or not has_dialog: return Prompt() try: return Dialog() except (dialog.ExecutableNotFound, UnsupportedTerminal): 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) elif 'width' in kwargs and 'height' not in kwargs: kwargs["width"], kwargs["height"] = guess_dimensions(text, width=kwargs["width"]) 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): """Ncurses interface using dialog.""" interactive = True def __init__(self): self.dialog = dialog.Dialog() exit = self.dialog.infobox("Setting up...") if exit != 0: raise UnsupportedTerminal @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 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(), break_long_words=False) def infobox(self, text, **kwargs): print "" print fill(text.strip(), break_long_words=False) 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 class UnsupportedTerminal(Error): """It doesn't look like we support this terminal. Internal error.""" pass