Added interaction to hooke.ui.commandline.LocalCommandExit.
[hooke.git] / hooke / ui / commandline.py
1 """Defines :class:`CommandLine` for driving Hooke from the command
2 line.
3 """
4
5 import cmd
6 import optparse
7 import readline # including readline makes cmd.Cmd.cmdloop() smarter
8 import shlex
9
10 from ..command import CommandExit, Exit, BooleanRequest, BooleanResponse, \
11     Command, Argument, StoreValue
12 from ..ui import UserInterface, CommandMessage
13
14
15 # Define a few helper classes.
16
17 class Default (object):
18     """Marker for options not given on the command line.
19     """
20     pass
21
22 class CommandLineParser (optparse.OptionParser):
23     """Implement a command line syntax for a
24     :class:`hooke.command.Command`.
25     """
26     def __init__(self, command, name_fn):
27         optparse.OptionParser.__init__(self, prog=name_fn(command.name))
28         self.command = command
29         self.command_opts = []
30         self.command_args = []
31         for a in command.arguments:
32             if a.name == 'help':
33                 continue # 'help' is a default OptionParser option
34             if a.optional == True:
35                 name = name_fn(a.name)
36                 self.add_option(
37                     '--%s' % name, dest=name, default=Default)
38                 self.command_opts.append(a)
39             else:
40                 self.command_args.append(a)
41
42     def exit(self, status=0, msg=None):
43         """Override :meth:`optparse.OptionParser.exit` which calls
44         :func:`sys.exit`.
45         """
46         if msg:
47             raise optparse.OptParseError(msg)
48         raise optparse.OptParseError('OptParse EXIT')
49
50 class CommandMethod (object):
51     """Base class for method replacer.
52
53     The .__call__ methods of `CommandMethod` subclasses functions will
54     provide the `do_*`, `help_*`, and `complete_*` methods of
55     :class:`HookeCmd`.
56     """
57     def __init__(self, cmd, command, name_fn):
58         self.cmd = cmd
59         self.command = command
60         self.name_fn = name_fn
61
62     def __call__(self, *args, **kwargs):
63         raise NotImplementedError
64
65 class DoCommand (CommandMethod):
66     def __init__(self, *args, **kwargs):
67         super(DoCommand, self).__init__(*args, **kwargs)
68         self.parser = CommandLineParser(self.command, self.name_fn)
69
70     def __call__(self, args):
71         try:
72             args = self._parse_args(args)
73         except optparse.OptParseError, e:
74             self.cmd.stdout.write(str(e).lstrip()+'\n')
75             self.cmd.stdout.write('Failure\n')
76             return
77         self.cmd.inqueue.put(CommandMessage(self.command, args))
78         while True:
79             msg = self.cmd.outqueue.get()
80             if isinstance(msg, Exit):
81                 return True
82             elif isinstance(msg, CommandExit):
83                 self.cmd.stdout.write(msg.__class__.__name__+'\n')
84                 self.cmd.stdout.write(str(msg).rstrip()+'\n')
85                 break
86             elif isinstance(msg, BooleanRequest):
87                 self._boolean_request(msg)
88                 continue
89             self.cmd.stdout.write(str(msg).rstrip()+'\n')
90
91     def _parse_args(self, args):
92         argv = shlex.split(args, comments=True, posix=True)
93         options,args = self.parser.parse_args(argv)
94         if len(args) != len(self.parser.command_args):
95             raise optparse.OptParseError('%d arguments given, but %s takes %d'
96                                          % (len(args),
97                                             self.name_fn(self.command.name),
98                                             len(self.parser.command_args)))
99         params = {}
100         for argument in self.parser.command_opts:
101             value = getattr(options, self.name_fn(argument.name))
102             if value != Default:
103                 params[argument.name] = value
104         for i,argument in enumerate(self.parser.command_args):
105             params[argument.name] = args[i]
106         return params
107
108     def _boolean_request(self, msg):
109         if msg.default == True:
110             yn = ' [Y/n] '
111         else:
112             yn = ' [y/N] '
113         self.cmd.stdout.write(msg.msg+yn)
114         response = self.cmd.stdin.readline().strip().lower()
115         if response.startswith('y'):
116             self.cmd.inqueue.put(BooleanResponse(True))
117         elif response.startswith('n'):
118             self.cmd.inqueue.put(BooleanResponse(False))
119         else:
120             self.cmd.inqueue.put(BooleanResponse(msg.default))
121
122 class HelpCommand (CommandMethod):
123     def __init__(self, *args, **kwargs):
124         super(HelpCommand, self).__init__(*args, **kwargs)
125         self.parser = CommandLineParser(self.command, self.name_fn)
126
127     def __call__(self):
128         blocks = [self.command.help(name_fn=self.name_fn),
129                   '------',
130                   'Usage: ' + self._usage_string(),
131                   '']
132         self.cmd.stdout.write('\n'.join(blocks))
133
134     def _message(self):
135         return self.command.help(name_fn=self.name_fn)
136
137     def _usage_string(self):
138         if len(self.parser.command_opts) == 0:
139             options_string = ''
140         else:
141             options_string = '[options]'
142         arg_string = ' '.join(
143             [self.name_fn(arg.name) for arg in self.parser.command_args])
144         return ' '.join([x for x in [self.parser.prog,
145                                      options_string,
146                                      arg_string]
147                          if x != ''])
148
149 class CompleteCommand (CommandMethod):
150     def __call__(self, text, line, begidx, endidx):
151         pass
152
153
154 # Define some additional commands
155
156 class LocalHelpCommand (Command):
157     """Called with an argument, prints that command's documentation.
158
159     With no argument, lists all available help topics as well as any
160     undocumented commands.
161     """
162     def __init__(self):
163         super(LocalHelpCommand, self).__init__(name='help', help=self.__doc__)
164         # We set .arguments now (vs. using th arguments option to __init__),
165         # to overwrite the default help argument.  We don't override
166         # :meth:`cmd.Cmd.do_help`, so `help --help` is not a valid command.
167         self.arguments = [
168             Argument(name='command', type='string', optional=True,
169                      help='The name of the command you want help with.')
170             ]
171
172     def _run(self, hooke, inqueue, outqueue, params):
173         raise NotImplementedError # cmd.Cmd already implements .do_help()
174
175 class LocalExitCommand (Command):
176     """Exit Hooke cleanly.
177     """
178     def __init__(self):
179         super(LocalExitCommand, self).__init__(
180             name='exit', aliases=['quit', 'EOF'], help=self.__doc__,
181             arguments = [
182                 Argument(name='force', type='bool', default=False,
183                          callback=StoreValue(True), help="""
184 Exit without prompting the user.  Use if you save often or don't make
185 typing mistakes ;).
186 """.strip()),
187                 ])
188
189     def _run(self, hooke, inqueue, outqueue, params):
190         """The guts of the `do_exit/_quit/_EOF` commands.
191
192         A `True` return stops :meth:`.cmdloop` execution.
193         """
194         _exit = True
195         if params['force'] == False:
196             # TODO: get results of hooke.playlists.current().is_saved()
197             is_saved = True
198             msg = 'Exit?'
199             default = True
200             if is_saved == False:
201                 msg = 'You did not save your playlist.  ' + msg
202                 default = False
203             outqueue.put(BooleanRequest(msg, default))
204             result = inqueue.get()
205             assert isinstance(result, BooleanResponse)
206             _exit = result.value
207         if _exit == True:
208             raise Exit()
209
210
211 # Now onto the main attraction.
212
213 class HookeCmd (cmd.Cmd):
214     def __init__(self, commands, inqueue, outqueue):
215         cmd.Cmd.__init__(self)
216         self.commands = commands
217         self.local_commands = [LocalExitCommand(), LocalHelpCommand()]
218         self.prompt = 'hooke> '
219         self._add_command_methods()
220         self.inqueue = inqueue
221         self.outqueue = outqueue
222
223     def _name_fn(self, name):
224         return name.replace(' ', '_')
225
226     def _add_command_methods(self):
227         for command in self.commands + self.local_commands:
228             for name in [command.name] + command.aliases:
229                 name = self._name_fn(name)
230                 setattr(self.__class__, 'help_%s' % name,
231                         HelpCommand(self, command, self._name_fn))
232                 if name != 'help':
233                     setattr(self.__class__, 'do_%s' % name,
234                             DoCommand(self, command, self._name_fn))
235                     setattr(self.__class__, 'complete_%s' % name,
236                             CompleteCommand(self, command, self._name_fn))
237
238
239 class CommandLine (UserInterface):
240     """Command line interface.  Simple and powerful.
241     """
242     def __init__(self):
243         super(CommandLine, self).__init__(name='command line')
244
245     def run(self, commands, ui_to_command_queue, command_to_ui_queue):
246         cmd = HookeCmd(commands,
247                        inqueue=ui_to_command_queue,
248                        outqueue=command_to_ui_queue)
249         cmd.cmdloop(self._splash_text())