Add hooke.playlist.NoteIndexList and refactor exception handling in commandline.
[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, Command, Argument
11 from ..compat.odict import odict
12 from ..ui import UserInterface, CommandMessage
13
14
15 # Define a few helper classes.
16
17 class CommandLineParser (optparse.OptionParser):
18     """Implement a command line syntax for a
19     :class:`hooke.command.Command`.
20     """
21     def __init__(self, command):
22         optparse.OptionParser.__init__(self, prog=command.name)
23         self.command = command
24         self.command_opts = odict()
25         self.command_args = odict()
26         for a in command.arguments:
27             if a.name == 'help':
28                 continue # 'help' is a default OptionParser option
29             name = a.name.replace('_', '-')
30             if a.optional == True:
31                 self.add_option('--%s' % name, dest=a.name, default=a.default)
32                 self.command_opts[name] = a
33             else:
34                 self.command_args[name] = a
35
36     def exit(self, status=0, msg=None):
37         """Override :meth:`optparse.OptionParser.exit` which calls
38         :func:`sys.exit`.
39         """
40         if msg:
41             raise optparse.OptParseError(msg)
42         raise optparse.OptParseError('OptParse EXIT')
43
44 class CommandMethod (object):
45     """Base class for method replacer.
46
47     The .__call__ methods of `CommandMethod` subclasses functions will
48     provide the `do_*`, `help_*`, and `complete_*` methods of
49     :class:`HookeCmd`.
50     """
51     def __init__(self, cmd, command):
52         self.cmd = cmd
53         self.command = command
54
55     def __call__(self, *args, **kwargs):
56         raise NotImplementedError
57
58 class DoCommand (CommandMethod):
59     def __init__(self, *args, **kwargs):
60         super(DoCommand, self).__init__(*args, **kwargs)
61         self.parser = CommandLineParser(self.command)
62
63     def __call__(self, args):
64         try:
65             args = self._parse_args(self.command, args)
66         except optparse.OptParseError, e:
67             self.cmd.stdout.write(str(e))
68             self.cmd.stdout.write('Failure\n')
69             return
70         self.cmd.inqueue.put(CommandMessage(self.command, args))
71         while True:
72             msg = self.cmd.outqueue.get()
73             if isinstance(msg, CommandExit):
74                 self.cmd.stdout.write(msg.__class__.__name__+'\n')
75                 self.cmd.stdout.write(str(msg).rstrip()+'\n')
76                 break
77             self.cmd.stdout.write(str(msg).rstrip()+'\n')
78
79     def _parse_args(self, command, args):
80         argv = shlex.split(args, comments=True, posix=True)
81         options,args = self.parser.parse_args(argv)
82         params = {}
83         for name,argument in self.parser.command_opts.items():
84             params[name] = getattr(options, name)
85         for name,argument in self.parser.command_args.items():
86             params[name] = value
87         return params
88
89 class HelpCommand (CommandMethod):
90     def __init__(self, *args, **kwargs):
91         super(HelpCommand, self).__init__(*args, **kwargs)
92         self.parser = CommandLineParser(self.command)
93
94     def __call__(self):
95         blocks = [self.command.help(),
96                   '------',
97                   'Usage: ' + self._usage_string(),
98                   '']
99         self.cmd.stdout.write('\n'.join(blocks))
100
101     def _message(self):
102         return self.command.help()
103
104     def _usage_string(self):
105         if len(self.parser.command_opts) == 0:
106             options_string = ''
107         else:
108             options_string = '[options]'
109         arg_string = ' '.join([name for name,arg
110                                in self.parser.command_args.items()])
111         return ' '.join([x for x in [self.command.name,
112                                      options_string,
113                                      arg_string]
114                          if x != ''])
115
116 class CompleteCommand (CommandMethod):
117     def __call__(self, text, line, begidx, endidx):
118         pass
119
120
121 # Now onto the main attraction.
122
123 class HookeCmd (cmd.Cmd):
124     def __init__(self, commands, inqueue, outqueue):
125         cmd.Cmd.__init__(self)
126         self.commands = commands
127         self.prompt = 'hooke> '
128         self._add_command_methods()
129         self.inqueue = inqueue
130         self.outqueue = outqueue
131
132     def _safe_name(self, name):
133         return name.lower().replace(' ', '_')
134
135     def _add_command_methods(self):
136         for command in self.commands:
137             command.name = self._safe_name(command.name)
138             command.aliases = [self._safe_name(n) for n in command.aliases]
139             for name in [command.name] + command.aliases:
140                 name = self._safe_name(name)
141                 setattr(self.__class__, 'do_%s' % name,
142                         DoCommand(self, command))
143                 setattr(self.__class__, 'help_%s' % name,
144                         HelpCommand(self, command))
145                 setattr(self.__class__, 'complete_%s' % name,
146                         CompleteCommand(self, command))
147
148         exit_command = Command(
149             name='exit', aliases=['quit', 'EOF'],
150             help='Exit Hooke cleanly.')
151         exit_command.arguments = [] # remove help argument
152         for name in [exit_command.name] + exit_command.aliases:
153             setattr(self.__class__, 'do_%s' % name,
154                     lambda self, args : True)
155             # the True return stops .cmdloop execution
156             setattr(self.__class__, 'help_%s' % name,
157                     HelpCommand(self, exit_command))
158
159         help_command = Command(
160             name='help',
161             help="""
162 Called with an argument, prints that command's documentation.
163
164 With no argument, lists all available help topics as well as any
165 undocumented commands.
166 """.strip())
167         help_command.arguments = [ # overwrite help argument
168             Argument(name='command', type='string', optional=True,
169                      help='The name of the command you want help with.')
170             ]
171         setattr(self.__class__, 'help_help',
172                 HelpCommand(self, help_command))
173
174
175 class CommandLine (UserInterface):
176     """Command line interface.  Simple and powerful.
177     """
178     def __init__(self):
179         super(CommandLine, self).__init__(name='command line')
180
181     def run(self, commands, ui_to_command_queue, command_to_ui_queue):
182         cmd = HookeCmd(commands,
183                        inqueue=ui_to_command_queue,
184                        outqueue=command_to_ui_queue)
185         cmd.cmdloop(self._splash_text())