]> scripts.mit.edu Git - wizard.git/blob - wizard/prompt.py
Implement interactive mode without validation.
[wizard.git] / wizard / prompt.py
1 """
2 Interface compatible with :class:`dialog.Dialog` for doing
3 non-ncurses interaction.
4
5 By convention, the last line of a text parameter should be
6 a short value with a trailing colon so that we can prompt a user
7 for a value immediately after it.
8 """
9
10 import sys
11 import readline
12 import decorator
13 import textwrap
14 import getpass
15
16 import wizard
17
18 try:
19     import dialog
20     has_dialog = True
21 except ImportError:
22     has_dialog = False
23
24 def fill(text, width=60):
25     return "\n\n".join(textwrap.fill(p, width=width) for p in text.split("\n\n"))
26
27 def guess_dimensions(text, width=60):
28     # +1 for the fact that there's no trailing newline from fill
29     # +2 for the borders
30     # +1 as a buffer in case we underestimate
31     return width, fill(text).count("\n", width-2) + 1 + 2 + 1
32
33 def make(prompt, non_interactive):
34     if non_interactive:
35         return FailPrompt()
36     if prompt or not has_dialog:
37         return Prompt()
38     try:
39         return Dialog()
40     except dialog.ExecutableNotFound:
41         return Prompt()
42
43 def join_or(items):
44     if len(items) == 0:
45         raise ValueError
46     elif len(items) == 1:
47         return items[0]
48     return ', '.join(items[:-1]) + ' or ' + items[-1]
49
50 @decorator.decorator
51 def dialog_wrap(f, self, text, *args, **kwargs):
52     if 'cmdopt' in kwargs: del kwargs['cmdopt']
53     if 'width' not in kwargs and 'height' not in kwargs:
54         kwargs["width"], kwargs["height"] = guess_dimensions(text)
55     result = f(self, text, *args, **kwargs)
56     if not isinstance(result, tuple):
57         exit = result
58         value = None
59     else:
60         exit, value = result
61     if exit == self.dialog.DIALOG_CANCEL or exit == self.dialog.DIALOG_ESC:
62         raise UserCancel
63     elif exit != self.dialog.DIALOG_OK:
64         # XXX: We don't support stuff like DIALOG_EXTRA or DIALOG_HELP
65         raise DialogError(exit)
66     return value
67
68 class Dialog(object):
69     interactive = True
70     """Ncurses interface using dialog."""
71     def __init__(self):
72         self.dialog = dialog.Dialog()
73     @dialog_wrap
74     def inputbox(self, *args, **kwargs):
75         kwargs.setdefault('initerror', "You cannot edit initial value; please type characters after it.")
76         initerror = kwargs['initerror']
77         del kwargs['initerror']
78         kwargs['height'] += 5 # for the text box
79         exit, result = self.dialog.inputbox(*args, **kwargs)
80         if exit == self.dialog.DIALOG_OK:
81             # do some funny munging
82             kwargs.setdefault('init', '')
83             if result[0:len(kwargs['init'])] != kwargs['init']:
84                 self.msgbox(initerror, height=10, width=50)
85                 exit = self.dialog.DIALOG_OK
86                 result = self.inputbox(*args, initerror=initerror, **kwargs)
87             else:
88                 result = result[len(kwargs['init']):]
89         return (exit, result)
90     @dialog_wrap
91     def menu(self, *args, **kwargs):
92         kwargs['height'] += 6 + len(kwargs['choices']) # for the border and menu entries
93         return self.dialog.menu(*args, **kwargs)
94     @dialog_wrap
95     def msgbox(self, *args, **kwargs):
96         kwargs['height'] += 3
97         return self.dialog.msgbox(*args, **kwargs)
98     @dialog_wrap
99     def passwordbox(self, *args, **kwargs):
100         kwargs['height'] += 6
101         return self.dialog.passwordbox(*args, **kwargs)
102     @dialog_wrap
103     def infobox(self, text, **kwargs):
104         return self.dialog.infobox(text, **kwargs)
105
106 @decorator.decorator
107 def prompt_wrap(f, self, *args, **kwargs):
108     try:
109         return f(self, *args, **kwargs)
110     except (EOFError, KeyboardInterrupt):
111         raise UserCancel
112
113 class Prompt(object):
114     interactive = True
115     @prompt_wrap
116     def inputbox(self, text, init='', **kwargs):
117         print ""
118         return raw_input(fill(text.strip()) + " " + init)
119     @prompt_wrap
120     def menu(self, text, choices=[], **kwargs):
121         print ""
122         print fill(text.strip())
123         values = list(choice[0] for choice in choices)
124         for choice in choices:
125             print "% 4s  %s" % (choice[0], choice[1])
126         while 1:
127             out = raw_input("Please enter %s: " % join_or(values))
128             if out not in values:
129                 print "'%s' is not a valid value" % out
130                 continue
131             return out
132     @prompt_wrap
133     def passwordbox(self, text, **kwargs):
134         print ""
135         return getpass.getpass(text + " ")
136     @prompt_wrap
137     def msgbox(self, text, **kwargs):
138         print ""
139         print fill(text.strip())
140         print "Press <Enter> to continue..."
141     @prompt_wrap
142     def infobox(self, text, **kwargs):
143         print ""
144         print fill(text.strip())
145
146 class FailPrompt(object):
147     """
148     Prompt that doesn't actually ask the user; just fails with
149     an error message.
150     """
151     interactive = False
152     def inputbox(self, *args, **kwargs):
153         kwargs.setdefault('cmdopt', '(unknown)')
154         raise MissingRequiredParam(kwargs['cmdopt'])
155     def passwordbox(self, *args, **kwargs):
156         kwargs.setdefault('cmdopt', '(unknown)')
157         raise MissingRequiredParam(kwargs['cmdopt'])
158     def menu(self, *args, **kwargs):
159         kwargs.setdefault('cmdopt', '(unknown)')
160         raise MissingRequiredParam(kwargs['cmdopt'])
161     def msgbox(self, text, **kwargs):
162         print ""
163         print fill(text.strip())
164     def infobox(self, text, **kwargs):
165         print ""
166         print fill(text.strip())
167
168 class Error(wizard.Error):
169     pass
170
171 class MissingRequiredParam(Error):
172     """Non-interactive, but we needed more info."""
173     def __init__(self, cmdopt):
174         self.cmdopt = cmdopt
175     def __str__(self):
176         return """
177
178 ERROR: Missing required parameter, try specifying %s""" % self.cmdopt
179
180 class UserCancel(Error):
181     """User canceled the input process."""
182     def __str__(self):
183         return "\nAborting installation process; no changes were made"
184
185 class DialogError(Error):
186     """Dialog returned a mysterious error."""
187     def __init__(self, exit):
188         self.exitcode = exit
189     def __str__(self):
190         return """
191
192 ERROR:  Dialog returned a mysterious exit code %d.  Please
193 send mail to scripts@mit.edu with the contents of this error
194 message and the preceding backtrace.
195 """ % self.exitcode
196