Add a `Delete` button to the GUI NavBar, and cleanup deletion callbacks.
[hooke.git] / hooke / ui / commandline.py
index 3ab755d61b6180c5175acebdf71b26ab7448491a..ecefd64cbcc566da0b0ae541a691a4286e52a970 100644 (file)
@@ -1,19 +1,54 @@
+# Copyright (C) 2010-2012 W. Trevor King <wking@drexel.edu>
+#
+# This file is part of Hooke.
+#
+# Hooke is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# Hooke is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Hooke.  If not, see <http://www.gnu.org/licenses/>.
+
 """Defines :class:`CommandLine` for driving Hooke from the command
 line.
 """
 
+import codecs
 import cmd
+import logging
 import optparse
-import readline # including readline makes cmd.Cmd.cmdloop() smarter
+import pprint
+try:
+    import readline # including readline makes cmd.Cmd.cmdloop() smarter
+except ImportError, e:
+    import logging
+    logging.warn('could not import readline, bash-like line editing disabled.')
 import shlex
+import sys
 
 from ..command import CommandExit, Exit, Command, Argument, StoreValue
-from ..interaction import Request, BooleanRequest
-from ..ui import UserInterface, CommandMessage
+from ..engine import CommandMessage, CloseEngine
+from ..interaction import EOFResponse, Request, ReloadUserInterfaceConfig
+from ..ui import UserInterface
+from ..util.convert import from_string
+from ..util.encoding import get_input_encoding, get_output_encoding
 
 
 # Define a few helper classes.
 
+class EOF (EOFError):
+    """Raise upon reaching the end of the input file.
+
+    After this point, no more user interaction is possible.
+    """
+    pass
+
 class Default (object):
     """Marker for options not given on the command line.
     """
@@ -33,8 +68,42 @@ class CommandLineParser (optparse.OptionParser):
                 continue # 'help' is a default OptionParser option
             if a.optional == True:
                 name = name_fn(a.name)
-                self.add_option(
-                    '--%s' % name, dest=name, default=Default)
+                type = a.type
+                if type == 'bool':
+                    if a.default == True:
+                        try:
+                            self.add_option(
+                                '--disable-%s' % name, dest=name,
+                                default=Default, action='store_false',
+                                help=self._argument_help(a))
+                        except optparse.OptionConflictError, e:
+                            logging.warn('error in %s: %s' % (command, e))
+                            raise
+                        self.command_opts.append(a)
+                        continue
+                    elif a.default == False:
+                        try:
+                            self.add_option(
+                                '--enable-%s' % name, dest=name,
+                                default=Default, action='store_true',
+                                help=self._argument_help(a))
+                        except optparse.OptionConflictError, e:
+                            logging.warn('error in %s: %s' % (command, e))
+                            raise
+                        self.command_opts.append(a)
+                        continue
+                    else:
+                        type = 'string'
+                elif type not in ['string', 'int', 'long', 'choice', 'float',
+                                  'complex']:
+                    type = 'string'
+                try:
+                    self.add_option(
+                        '--%s' % name, dest=name, type=type, default=Default,
+                        help=self._argument_help(a))
+                except optparse.OptionConflictError, e:
+                    logging.warn('error in %s: %s' % (command, e))
+                    raise
                 self.command_opts.append(a)
             else:
                 self.command_args.append(a)
@@ -47,6 +116,10 @@ class CommandLineParser (optparse.OptionParser):
             self.command_args.remove(infinite_counter)
             self.command_args.append(infinite_counter)
 
+    def _argument_help(self, argument):
+        return '%s (%s)' % (argument._help, argument.default)
+        # default in the case of callbacks, config-backed values, etc.?
+
     def exit(self, status=0, msg=None):
         """Override :meth:`optparse.OptionParser.exit` which calls
         :func:`sys.exit`.
@@ -79,26 +152,36 @@ class DoCommand (CommandMethod):
         try:
             args = self._parse_args(args)
         except optparse.OptParseError, e:
-            self.cmd.stdout.write(str(e).lstrip()+'\n')
+            self.cmd.stdout.write(unicode(e).lstrip()+'\n')
             self.cmd.stdout.write('Failure\n')
             return
-        self.cmd.inqueue.put(CommandMessage(self.command, args))
+        cm = CommandMessage(self.command.name, args)
+        self.cmd.ui._submit_command(cm, self.cmd.inqueue)
         while True:
             msg = self.cmd.outqueue.get()
             if isinstance(msg, Exit):
                 return True
             elif isinstance(msg, CommandExit):
                 self.cmd.stdout.write(msg.__class__.__name__+'\n')
-                self.cmd.stdout.write(str(msg).rstrip()+'\n')
+                self.cmd.stdout.write(unicode(msg).rstrip()+'\n')
                 break
+            elif isinstance(msg, ReloadUserInterfaceConfig):
+                self.cmd.ui.reload_config(msg.config)
+                continue
             elif isinstance(msg, Request):
-                self._handle_request(msg)
+                try:
+                    self._handle_request(msg)
+                except EOF:
+                    return True
                 continue
-            self.cmd.stdout.write(str(msg).rstrip()+'\n')
+            if isinstance(msg, dict):
+                text = pprint.pformat(msg)
+            else:
+                text = unicode(msg)
+            self.cmd.stdout.write(text.rstrip()+'\n')
 
     def _parse_args(self, args):
-        argv = shlex.split(args, comments=True, posix=True)
-        options,args = self.parser.parse_args(argv)
+        options,args = self.parser.parse_args(args)
         self._check_argument_length_bounds(args)
         params = {}
         for argument in self.parser.command_opts:
@@ -108,12 +191,15 @@ class DoCommand (CommandMethod):
         arg_index = 0
         for argument in self.parser.command_args:
             if argument.count == 1:
-                params[argument.name] = args[arg_index]
+                params[argument.name] = from_string(args[arg_index],
+                                                    argument.type)
             elif argument.count > 1:
-                params[argument.name] = \
-                    args[arg_index:arg_index+argument.count]
+                params[argument.name] = [
+                    from_string(a, argument.type)
+                    for a in args[arg_index:arg_index+argument.count]]
             else: # argument.count == -1:
-                params[argument.name] = args[arg_index:]
+                params[argument.name] = [
+                    from_string(a, argument.type) for a in args[arg_index:]]
             arg_index += argument.count
         return params
 
@@ -124,7 +210,7 @@ class DoCommand (CommandMethod):
         If not, raise optparse.OptParseError().
         """
         min_args = 0
-        max_args = -1
+        max_args = 0
         for argument in self.parser.command_args:
             if argument.optional == False and argument.count > 0:
                 min_args += argument.count
@@ -160,9 +246,19 @@ class DoCommand (CommandMethod):
         while True:
             if error != None:
                 self.cmd.stdout.write(''.join([
-                        error.__class__.__name__, ': ', str(error), '\n']))
+                        error.__class__.__name__, ': ', unicode(error), '\n']))
             self.cmd.stdout.write(prompt_string)
-            value = parser(msg, self.cmd.stdin.readline())
+            stdin = sys.stdin
+            try:
+                sys.stdin = self.cmd.stdin
+                raw_response = raw_input()
+            except EOFError, e:
+                self.cmd.inqueue.put(EOFResponse())
+                self.cmd.inqueue.put(CloseEngine())
+                raise EOF
+            finally:
+                sys.stdin = stdin
+            value = parser(msg, raw_response)
             try:
                 response = msg.response(value)
                 break
@@ -195,12 +291,17 @@ class DoCommand (CommandMethod):
         return msg.msg + d
 
     def _string_request_parser(self, msg, response):
+        response = response.strip()
+        if response == '':
+            return msg.default
         return response.strip()
 
     def _float_request_prompt(self, msg):
         return self._string_request_prompt(msg)
 
     def _float_request_parser(self, msg, resposne):
+        if response.strip() == '':
+            return msg.default
         return float(response)
 
     def _selection_request_prompt(self, msg):
@@ -212,26 +313,49 @@ class DoCommand (CommandMethod):
             prompt = '? '
         else:
             prompt = '? [%d] ' % msg.default
-        return '\n'.join([msg,options,prompt])
+        return '\n'.join([msg.msg,options,prompt])
     
     def _selection_request_parser(self, msg, response):
+        if response.strip() == '':
+            return msg.default
+        return int(response)
+
+    def _point_request_prompt(self, msg):
+        block = msg.curve.data[msg.block]
+        block_info = ('(curve: %s, block: %s, %d points)'
+                      % (msg.curve.name,
+                         block.info['name'],
+                         block.shape[0]))
+
+        if msg.default == None:
+            prompt = '? '
+        else:
+            prompt = '? [%d] ' % msg.default
+        return ' '.join([msg.msg,block_info,prompt])
+    
+    def _point_request_parser(self, msg, response):
+        if response.strip() == '':
+            return msg.default
         return int(response)
 
 
 class HelpCommand (CommandMethod):
+    """Supersedes :class:`hooke.plugin.engine.HelpCommand`.
+    """
     def __init__(self, *args, **kwargs):
         super(HelpCommand, self).__init__(*args, **kwargs)
         self.parser = CommandLineParser(self.command, self.name_fn)
 
     def __call__(self):
-        blocks = [self.command.help(name_fn=self.name_fn),
+        blocks = [self.parser.format_help(),
+                  self._command_message(),
                   '----',
                   'Usage: ' + self._usage_string(),
                   '']
         self.cmd.stdout.write('\n'.join(blocks))
 
-    def _message(self):
-        return self.command.help(name_fn=self.name_fn)
+    def _command_message(self):
+        return self.command._help
 
     def _usage_string(self):
         if len(self.parser.command_opts) == 0:
@@ -250,70 +374,14 @@ class CompleteCommand (CommandMethod):
         pass
 
 
-# Define some additional commands
-
-class LocalHelpCommand (Command):
-    """Called with an argument, prints that command's documentation.
-
-    With no argument, lists all available help topics as well as any
-    undocumented commands.
-    """
-    def __init__(self):
-        super(LocalHelpCommand, self).__init__(name='help', help=self.__doc__)
-        # We set .arguments now (vs. using th arguments option to __init__),
-        # to overwrite the default help argument.  We don't override
-        # :meth:`cmd.Cmd.do_help`, so `help --help` is not a valid command.
-        self.arguments = [
-            Argument(name='command', type='string', optional=True,
-                     help='The name of the command you want help with.')
-            ]
-
-    def _run(self, hooke, inqueue, outqueue, params):
-        raise NotImplementedError # cmd.Cmd already implements .do_help()
-
-class LocalExitCommand (Command):
-    """Exit Hooke cleanly.
-    """
-    def __init__(self):
-        super(LocalExitCommand, self).__init__(
-            name='exit', aliases=['quit', 'EOF'], help=self.__doc__,
-            arguments = [
-                Argument(name='force', type='bool', default=False,
-                         help="""
-Exit without prompting the user.  Use if you save often or don't make
-typing mistakes ;).
-""".strip()),
-                ])
-
-    def _run(self, hooke, inqueue, outqueue, params):
-        """The guts of the `do_exit/_quit/_EOF` commands.
-
-        A `True` return stops :meth:`.cmdloop` execution.
-        """
-        _exit = True
-        if params['force'] == False:
-            # TODO: get results of hooke.playlists.current().is_saved()
-            is_saved = True
-            msg = 'Exit?'
-            default = True
-            if is_saved == False:
-                msg = 'You did not save your playlist.  ' + msg
-                default = False
-            outqueue.put(BooleanRequest(msg, default))
-            result = inqueue.get()
-            assert result.type == 'boolean'
-            _exit = result.value
-        if _exit == True:
-            raise Exit()
-
 
 # Now onto the main attraction.
 
 class HookeCmd (cmd.Cmd):
-    def __init__(self, commands, inqueue, outqueue):
+    def __init__(self, ui, commands, inqueue, outqueue):
         cmd.Cmd.__init__(self)
+        self.ui = ui
         self.commands = commands
-        self.local_commands = [LocalExitCommand(), LocalHelpCommand()]
         self.prompt = 'hooke> '
         self._add_command_methods()
         self.inqueue = inqueue
@@ -323,7 +391,9 @@ class HookeCmd (cmd.Cmd):
         return name.replace(' ', '_')
 
     def _add_command_methods(self):
-        for command in self.commands + self.local_commands:
+        for command in self.commands:
+            if command.name == 'exit':
+                command.aliases.extend(['quit', 'EOF'])
             for name in [command.name] + command.aliases:
                 name = self._name_fn(name)
                 setattr(self.__class__, 'help_%s' % name,
@@ -334,6 +404,45 @@ class HookeCmd (cmd.Cmd):
                     setattr(self.__class__, 'complete_%s' % name,
                             CompleteCommand(self, command, self._name_fn))
 
+    def parseline(self, line):
+        """Override Cmd.parseline to use shlex.split.
+
+        Notes
+        -----
+        This allows us to handle comments cleanly.  With the default
+        Cmd implementation, a pure comment line will call the .default
+        error message.
+
+        Since we use shlex to strip comments, we return a list of
+        split arguments rather than the raw argument string.
+        """
+        line = line.strip()
+        argv = shlex.split(line, comments=True, posix=True)
+        if len(argv) == 0:
+            return None, None, '' # return an empty line
+        cmd = argv[0]
+        args = argv[1:]
+        if cmd == '?':
+            cmd = 'help'
+        elif cmd == '!':
+            cmd = 'system'
+        return cmd, args, line
+
+    def do_help(self, arg):
+        """Wrap Cmd.do_help to handle our .parseline argument list.
+        """
+        if len(arg) == 0:
+            return cmd.Cmd.do_help(self, '')
+        return cmd.Cmd.do_help(self, arg[0])
+
+    def emptyline(self):
+        """Override Cmd.emptyline to not do anything.
+
+        Repeating the last non-empty command seems unwise.  Explicit
+        is better than implicit.
+        """
+        pass
+
 
 class CommandLine (UserInterface):
     """Command line interface.  Simple and powerful.
@@ -341,8 +450,22 @@ class CommandLine (UserInterface):
     def __init__(self):
         super(CommandLine, self).__init__(name='command line')
 
-    def run(self, commands, ui_to_command_queue, command_to_ui_queue):
-        cmd = HookeCmd(commands,
+    def _cmd(self, commands, ui_to_command_queue, command_to_ui_queue):
+        cmd = HookeCmd(self, commands,
                        inqueue=ui_to_command_queue,
                        outqueue=command_to_ui_queue)
-        cmd.cmdloop(self._splash_text())
+        #cmd.stdin = codecs.getreader(get_input_encoding())(cmd.stdin)
+        cmd.stdout = codecs.getwriter(get_output_encoding())(cmd.stdout)
+        return cmd
+
+    def run(self, commands, ui_to_command_queue, command_to_ui_queue):
+        cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
+        cmd.cmdloop(self._splash_text(extra_info={
+                    'get-details':'run `license`',
+                    }))
+
+    def run_lines(self, commands, ui_to_command_queue, command_to_ui_queue,
+                  lines):
+        cmd = self._cmd(commands, ui_to_command_queue, command_to_ui_queue)
+        for line in lines:
+            cmd.onecmd(line)