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