Fix hooke -> commands typos in ui and ui.commandline.
[hooke.git] / hooke / ui / commandline.py
1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or
6 # modify it under the terms of the GNU Lesser General Public
7 # License as published by the Free Software Foundation, either
8 # version 3 of the License, or (at your option) any later version.
9 #
10 # Hooke is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU Lesser General Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 """Defines :class:`CommandLine` for driving Hooke from the command
20 line.
21 """
22
23 import codecs
24 import cmd
25 import optparse
26 import readline # including readline makes cmd.Cmd.cmdloop() smarter
27 import shlex
28
29 from ..command import CommandExit, Exit, Command, Argument, StoreValue
30 from ..interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
31 from ..ui import UserInterface, CommandMessage
32 from ..util.encoding import get_input_encoding, get_output_encoding
33
34
35 # Define a few helper classes.
36
37 class Default (object):
38     """Marker for options not given on the command line.
39     """
40     pass
41
42 class CommandLineParser (optparse.OptionParser):
43     """Implement a command line syntax for a
44     :class:`hooke.command.Command`.
45     """
46     def __init__(self, command, name_fn):
47         optparse.OptionParser.__init__(self, prog=name_fn(command.name))
48         self.command = command
49         self.command_opts = []
50         self.command_args = []
51         for a in command.arguments:
52             if a.name == 'help':
53                 continue # 'help' is a default OptionParser option
54             if a.optional == True:
55                 name = name_fn(a.name)
56                 self.add_option(
57                     '--%s' % name, dest=name, default=Default)
58                 self.command_opts.append(a)
59             else:
60                 self.command_args.append(a)
61         infinite_counters = [a for a in self.command_args if a.count == -1]
62         assert len(infinite_counters) <= 1, \
63             'Multiple infinite counts for %s: %s\nNeed a better CommandLineParser implementation.' \
64             % (command.name, ', '.join([a.name for a in infinite_counters]))
65         if len(infinite_counters) == 1: # move the big counter to the end.
66             infinite_counter = infinite_counters[0]
67             self.command_args.remove(infinite_counter)
68             self.command_args.append(infinite_counter)
69
70     def exit(self, status=0, msg=None):
71         """Override :meth:`optparse.OptionParser.exit` which calls
72         :func:`sys.exit`.
73         """
74         if msg:
75             raise optparse.OptParseError(msg)
76         raise optparse.OptParseError('OptParse EXIT')
77
78 class CommandMethod (object):
79     """Base class for method replacer.
80
81     The .__call__ methods of `CommandMethod` subclasses functions will
82     provide the `do_*`, `help_*`, and `complete_*` methods of
83     :class:`HookeCmd`.
84     """
85     def __init__(self, cmd, command, name_fn):
86         self.cmd = cmd
87         self.command = command
88         self.name_fn = name_fn
89
90     def __call__(self, *args, **kwargs):
91         raise NotImplementedError
92
93 class DoCommand (CommandMethod):
94     def __init__(self, *args, **kwargs):
95         super(DoCommand, self).__init__(*args, **kwargs)
96         self.parser = CommandLineParser(self.command, self.name_fn)
97
98     def __call__(self, args):
99         try:
100             args = self._parse_args(args)
101         except optparse.OptParseError, e:
102             self.cmd.stdout.write(str(e).lstrip()+'\n')
103             self.cmd.stdout.write('Failure\n')
104             return
105         self.cmd.inqueue.put(CommandMessage(self.command, args))
106         while True:
107             msg = self.cmd.outqueue.get()
108             if isinstance(msg, Exit):
109                 return True
110             elif isinstance(msg, CommandExit):
111                 self.cmd.stdout.write(msg.__class__.__name__+'\n')
112                 self.cmd.stdout.write(str(msg).rstrip()+'\n')
113                 break
114             elif isinstance(msg, ReloadUserInterfaceConfig):
115                 self.cmd.ui.reload_config(msg.config)
116                 continue
117             elif isinstance(msg, Request):
118                 self._handle_request(msg)
119                 continue
120             self.cmd.stdout.write(str(msg).rstrip()+'\n')
121
122     def _parse_args(self, args):
123         options,args = self.parser.parse_args(args)
124         self._check_argument_length_bounds(args)
125         params = {}
126         for argument in self.parser.command_opts:
127             value = getattr(options, self.name_fn(argument.name))
128             if value != Default:
129                 params[argument.name] = value
130         arg_index = 0
131         for argument in self.parser.command_args:
132             if argument.count == 1:
133                 params[argument.name] = args[arg_index]
134             elif argument.count > 1:
135                 params[argument.name] = \
136                     args[arg_index:arg_index+argument.count]
137             else: # argument.count == -1:
138                 params[argument.name] = args[arg_index:]
139             arg_index += argument.count
140         return params
141
142     def _check_argument_length_bounds(self, arguments):
143         """Check that there are an appropriate number of arguments in
144         `args`.
145
146         If not, raise optparse.OptParseError().
147         """
148         min_args = 0
149         max_args = 0
150         for argument in self.parser.command_args:
151             if argument.optional == False and argument.count > 0:
152                 min_args += argument.count
153             if max_args >= 0: # otherwise already infinite
154                 if argument.count == -1:
155                     max_args = -1
156                 else:
157                     max_args += argument.count
158         if len(arguments) < min_args \
159                 or (max_args >= 0 and len(arguments) > max_args):
160             if min_args == max_args:
161                 target_string = str(min_args)
162             elif max_args == -1:
163                 target_string = 'more than %d' % min_args
164             else:
165                 target_string = '%d to %d' % (min_args, max_args)
166             raise optparse.OptParseError(
167                 '%d arguments given, but %s takes %s'
168                 % (len(arguments), self.name_fn(self.command.name),
169                    target_string))
170
171     def _handle_request(self, msg):
172         """Repeatedly try to get a response to `msg`.
173         """
174         prompt = getattr(self, '_%s_request_prompt' % msg.type, None)
175         if prompt == None:
176             raise NotImplementedError('_%s_request_prompt' % msg.type)
177         prompt_string = prompt(msg)
178         parser = getattr(self, '_%s_request_parser' % msg.type, None)
179         if parser == None:
180             raise NotImplementedError('_%s_request_parser' % msg.type)
181         error = None
182         while True:
183             if error != None:
184                 self.cmd.stdout.write(''.join([
185                         error.__class__.__name__, ': ', str(error), '\n']))
186             self.cmd.stdout.write(prompt_string)
187             value = parser(msg, self.cmd.stdin.readline())
188             try:
189                 response = msg.response(value)
190                 break
191             except ValueError, error:
192                 continue
193         self.cmd.inqueue.put(response)
194
195     def _boolean_request_prompt(self, msg):
196         if msg.default == True:
197             yn = ' [Y/n] '
198         else:
199             yn = ' [y/N] '
200         return msg.msg + yn
201
202     def _boolean_request_parser(self, msg, response):
203         value = response.strip().lower()
204         if value.startswith('y'):
205             value = True
206         elif value.startswith('n'):
207             value = False
208         elif len(value) == 0:
209             value = msg.default
210         return value
211
212     def _string_request_prompt(self, msg):
213         if msg.default == None:
214             d = ' '
215         else:
216             d = ' [%s] ' % msg.default
217         return msg.msg + d
218
219     def _string_request_parser(self, msg, response):
220         return response.strip()
221
222     def _float_request_prompt(self, msg):
223         return self._string_request_prompt(msg)
224
225     def _float_request_parser(self, msg, resposne):
226         return float(response)
227
228     def _selection_request_prompt(self, msg):
229         options = []
230         for i,option in enumerate(msg.options):
231             options.append('   %d) %s' % (i,option))
232         options = ''.join(options)
233         if msg.default == None:
234             prompt = '? '
235         else:
236             prompt = '? [%d] ' % msg.default
237         return '\n'.join([msg,options,prompt])
238     
239     def _selection_request_parser(self, msg, response):
240         return int(response)
241
242
243 class HelpCommand (CommandMethod):
244     def __init__(self, *args, **kwargs):
245         super(HelpCommand, self).__init__(*args, **kwargs)
246         self.parser = CommandLineParser(self.command, self.name_fn)
247
248     def __call__(self):
249         blocks = [self.command.help(name_fn=self.name_fn),
250                   '----',
251                   'Usage: ' + self._usage_string(),
252                   '']
253         self.cmd.stdout.write('\n'.join(blocks))
254
255     def _message(self):
256         return self.command.help(name_fn=self.name_fn)
257
258     def _usage_string(self):
259         if len(self.parser.command_opts) == 0:
260             options_string = ''
261         else:
262             options_string = '[options]'
263         arg_string = ' '.join(
264             [self.name_fn(arg.name) for arg in self.parser.command_args])
265         return ' '.join([x for x in [self.parser.prog,
266                                      options_string,
267                                      arg_string]
268                          if x != ''])
269
270 class CompleteCommand (CommandMethod):
271     def __call__(self, text, line, begidx, endidx):
272         pass
273
274
275 # Define some additional commands
276
277 class LocalHelpCommand (Command):
278     """Called with an argument, prints that command's documentation.
279
280     With no argument, lists all available help topics as well as any
281     undocumented commands.
282     """
283     def __init__(self):
284         super(LocalHelpCommand, self).__init__(name='help', help=self.__doc__)
285         # We set .arguments now (vs. using th arguments option to __init__),
286         # to overwrite the default help argument.  We don't override
287         # :meth:`cmd.Cmd.do_help`, so `help --help` is not a valid command.
288         self.arguments = [
289             Argument(name='command', type='string', optional=True,
290                      help='The name of the command you want help with.')
291             ]
292
293     def _run(self, commands, inqueue, outqueue, params):
294         raise NotImplementedError # cmd.Cmd already implements .do_help()
295
296 class LocalExitCommand (Command):
297     """Exit Hooke cleanly.
298     """
299     def __init__(self):
300         super(LocalExitCommand, self).__init__(
301             name='exit', aliases=['quit', 'EOF'], help=self.__doc__,
302             arguments = [
303                 Argument(name='force', type='bool', default=False,
304                          help="""
305 Exit without prompting the user.  Use if you save often or don't make
306 typing mistakes ;).
307 """.strip()),
308                 ])
309
310     def _run(self, commands, inqueue, outqueue, params):
311         """The guts of the `do_exit/_quit/_EOF` commands.
312
313         A `True` return stops :meth:`.cmdloop` execution.
314         """
315         _exit = True
316         if params['force'] == False:
317             not_saved = [p.name for p in hooke.playlists
318                          if p.is_saved() == False]
319             msg = 'Exit?'
320             default = True
321             if len(not_saved) > 0:
322                 msg = 'Unsaved playlists (%s).  %s' \
323                     % (', '.join([str(p) for p in not_saved]), msg)
324                 default = False
325             outqueue.put(BooleanRequest(msg, default))
326             result = inqueue.get()
327             assert result.type == 'boolean'
328             _exit = result.value
329         if _exit == True:
330             raise Exit()
331
332
333 # Now onto the main attraction.
334
335 class HookeCmd (cmd.Cmd):
336     def __init__(self, ui, commands, inqueue, outqueue):
337         cmd.Cmd.__init__(self)
338         self.ui = ui
339         self.commands = commands
340         self.local_commands = [LocalExitCommand(), LocalHelpCommand()]
341         self.prompt = 'hooke> '
342         self._add_command_methods()
343         self.inqueue = inqueue
344         self.outqueue = outqueue
345
346     def _name_fn(self, name):
347         return name.replace(' ', '_')
348
349     def _add_command_methods(self):
350         for command in self.commands + self.local_commands:
351             for name in [command.name] + command.aliases:
352                 name = self._name_fn(name)
353                 setattr(self.__class__, 'help_%s' % name,
354                         HelpCommand(self, command, self._name_fn))
355                 if name != 'help':
356                     setattr(self.__class__, 'do_%s' % name,
357                             DoCommand(self, command, self._name_fn))
358                     setattr(self.__class__, 'complete_%s' % name,
359                             CompleteCommand(self, command, self._name_fn))
360
361     def parseline(self, line):
362         """Override Cmd.parseline to use shlex.split.
363
364         Notes
365         -----
366         This allows us to handle comments cleanly.  With the default
367         Cmd implementation, a pure comment line will call the .default
368         error message.
369
370         Since we use shlex to strip comments, we return a list of
371         split arguments rather than the raw argument string.
372         """
373         line = line.strip()
374         argv = shlex.split(line, comments=True, posix=True)
375         if len(argv) == 0:
376             return None, None, '' # return an empty line
377         elif argv[0] == '?':
378             argv[0] = 'help'
379         elif argv[0] == '!':
380             argv[0] = 'system'
381         return argv[0], argv[1:], line
382
383     def do_help(self, arg):
384         """Wrap Cmd.do_help to handle our .parseline argument list.
385         """
386         if len(arg) == 0:
387             return cmd.Cmd.do_help(self, '')
388         return cmd.Cmd.do_help(self, arg[0])
389
390     def empytline(self):
391         """Override Cmd.emptyline to not do anything.
392
393         Repeating the last non-empty command seems unwise.  Explicit
394         is better than implicit.
395         """
396         pass
397
398 class CommandLine (UserInterface):
399     """Command line interface.  Simple and powerful.
400     """
401     def __init__(self):
402         super(CommandLine, self).__init__(name='command line')
403
404     def _cmd(self, commands, ui_to_command_queue, command_to_ui_queue):
405         cmd = HookeCmd(self, commands,
406                        inqueue=ui_to_command_queue,
407                        outqueue=command_to_ui_queue)
408         #cmd.stdin = codecs.getreader(get_input_encoding())(cmd.stdin)
409         cmd.stdout = codecs.getwriter(get_output_encoding())(cmd.stdout)
410         return cmd
411
412     def run(self, commands, ui_to_command_queue, command_to_ui_queue):
413         cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
414         cmd.cmdloop(self._splash_text())
415
416     def run_lines(self, commands, ui_to_command_queue, command_to_ui_queue,
417                   lines):
418         cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
419         for line in lines:
420             cmd.onecmd(line)