f87f082438fe0772763397ed36f7edf5e51df2b6
[hooke.git] / hooke / plugin / command_stack.py
1 # Copyright (C) 2010-2011 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 modify it
6 # under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
13 # 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 """The ``command_stack`` module provides :class:`CommandStackPlugin`
20 and several associated :class:`~hooke.command.Command`\s exposing
21 :mod`hooke.command_stack`'s functionality.
22 """
23
24 import logging
25 import os.path
26 from Queue import Queue
27
28 from ..command import Command, Argument, Success, Failure
29 from ..command_stack import FileCommandStack
30 from ..config import Setting
31 from ..engine import CloseEngine, CommandMessage
32 from . import Builtin
33
34
35 # Define useful command subclasses
36
37 class CommandStackCommand (Command):
38     """Subclass to avoid pushing control commands to the stack.
39     """
40     def __init__(self, *args, **kwargs):
41         super(CommandStackCommand, self).__init__(*args, **kwargs)
42         stack = [a for a in self.arguments if a.name == 'stack'][0]
43         stack.default = False
44
45     def _set_state(self, state):
46         try:
47             self.plugin.set_state(state)
48         except ValueError, e:
49             self.plugin.log('raising error: %s' % e)
50             raise Failure('invalid state change: %s' % e.state_change)
51
52
53 class CaptureCommand (CommandStackCommand):
54     """Run a mock-engine and save the incoming commands.
55
56     Notes
57     -----
58     Due to limitations in the --script and --command option
59     implementations in ./bin/hooke, capture sessions will die at the
60     end of the script and command execution before entering
61     --persist's interactive session.
62     """
63     def __init__(self, name, plugin):
64         super(CaptureCommand, self).__init__(
65             name=name, help=self.__doc__, plugin=plugin)
66
67     def _run(self, hooke, inqueue, outqueue, params):
68         # TODO: possibly merge code with CommandEngine.run()
69
70         # Fake successful completion so UI continues sending commands.
71         outqueue.put(Success())
72
73         while True:
74             msg = inqueue.get()
75             if isinstance(msg, CloseEngine):
76                 outqueue.put('CloseEngine')
77                 inqueue.put(msg)  # Put CloseEngine back for CommandEngine.
78                 self._set_state('inactive')
79                 return
80             assert isinstance(msg, CommandMessage), type(msg)
81             cmd = hooke.command_by_name[msg.command]
82             if (msg.explicit_user_call == False
83                 or isinstance(cmd, CommandStackCommand)):
84                 if isinstance(cmd, StopCaptureCommand):
85                     outqueue = Queue()  # Grab StopCaptureCommand's completion.
86                 cmd.run(hooke, inqueue, outqueue, **msg.arguments)
87                 if isinstance(cmd, StopCaptureCommand):
88                     assert self.plugin.state == 'inactive', self.plugin.state
89                     # Return the stolen completion as our own.
90                     raise outqueue.get(block=False)
91             else:
92                 self.plugin.log('appending %s' % msg)
93                 self.plugin.command_stack.append(msg)
94                 # Fake successful completion so UI continues sending commands.
95                 outqueue.put(Success())
96
97
98 # The plugin itself
99
100 class CommandStackPlugin (Builtin):
101     """Commands for managing a command stack (similar to macros).
102     """
103     def __init__(self):
104         super(CommandStackPlugin, self).__init__(name='command_stack')
105         self._commands = [
106             StartCaptureCommand(self), StopCaptureCommand(self),
107             ReStartCaptureCommand(self),
108             PopCommand(self), GetCommand(self), GetStateCommand(self),
109             SaveCommand(self), LoadCommand(self), ExecuteCommand(self),
110             ]
111         self._settings = [
112             Setting(section=self.setting_section, help=self.__doc__),
113             Setting(section=self.setting_section, option='path',
114                     value=os.path.join('resources', 'command_stack'),
115                     help='Directory containing command stack files.'), # TODO: allow colon separated list, like $PATH.
116             ]
117         self.command_stack = FileCommandStack()
118         self.state = 'inactive'
119         # inactive <-> active.
120         self._valid_transitions = {
121             'inactive': ['active'],
122             'active': ['inactive'],
123             }
124
125     def default_settings(self):
126         return self._settings
127
128     def log(self, msg):
129         log = logging.getLogger('hooke')
130         log.debug('%s %s' % (self.name, msg))
131
132     def set_state(self, state):
133         state_change = '%s -> %s' % (self.state, state)
134         self.log('changing state: %s' % state_change)
135         if state not in self._valid_transitions[self.state]:
136             e = ValueError(state)
137             e.state_change = state_change
138             raise e
139         self.state = state
140
141
142 # Define commands
143
144 class StartCaptureCommand (CaptureCommand):
145     """Clear any previous stack and run the mock-engine.
146     """
147     def __init__(self, plugin):
148         super(StartCaptureCommand, self).__init__(
149             name='start command capture', plugin=plugin)
150
151     def _run(self, hooke, inqueue, outqueue, params):
152         self._set_state('active')
153         self.plugin.command_stack = FileCommandStack()  # clear command stack
154         super(StartCaptureCommand, self)._run(hooke, inqueue, outqueue, params)
155
156
157 class ReStartCaptureCommand (CaptureCommand):
158     """Run the mock-engine.
159     """
160     def __init__(self, plugin):
161         super(ReStartCaptureCommand, self).__init__(
162             name='restart command capture', plugin=plugin)
163
164     def _run(self, hooke, inqueue, outqueue, params):
165         self._set_state('active')
166         super(ReStartCaptureCommand, self)._run(hooke, inqueue, outqueue, params)
167
168
169 class StopCaptureCommand (CommandStackCommand):
170     """Stop the mock-engine.
171     """
172     def __init__(self, plugin):
173         super(StopCaptureCommand, self).__init__(
174             name='stop command capture', help=self.__doc__, plugin=plugin)
175
176     def _run(self, hooke, inqueue, outqueue, params):
177         self._set_state('inactive')
178
179
180 class PopCommand (CommandStackCommand):
181     """Pop the top command off the stack.
182     """
183     def __init__(self, plugin):
184         super(PopCommand, self).__init__(
185             name='pop command from stack', help=self.__doc__, plugin=plugin)
186
187     def _run(self, hooke, inqueue, outqueue, params):
188         outqueue.put(self.plugin.command_stack.pop())
189
190
191 class GetCommand (CommandStackCommand):
192     """Return the command stack.
193     """
194     def __init__(self, plugin):
195         super(GetCommand, self).__init__(
196             name='get command stack', help=self.__doc__, plugin=plugin)
197
198     def _run(self, hooke, inqueue, outqueue, params):
199         outqueue.put(self.plugin.command_stack)
200
201 class GetStateCommand (CommandStackCommand):
202     """Return the mock-engine state.
203     """
204     def __init__(self, plugin):
205         super(GetStateCommand, self).__init__(
206             name='get command capture state', help=self.__doc__, plugin=plugin)
207
208     def _run(self, hooke, inqueue, outqueue, params):
209         outqueue.put(self.plugin.state)
210
211
212 class SaveCommand (CommandStackCommand):
213     """Save the command stack.
214     """
215     def __init__(self, plugin):
216         super(SaveCommand, self).__init__(
217             name='save command stack',
218             arguments=[
219                 Argument(name='output', type='file',
220                          help="""
221 File name for the output command stack.  Defaults to overwriting the
222 input command stack.  If the command stack does not have an input file
223 (e.g. it is the default) then the file name defaults to `default`.
224 """.strip()),
225                 ],
226             help=self.__doc__, plugin=plugin)
227
228     def _run(self, hooke, inqueue, outqueue, params):
229         params = self._setup_params(hooke, params)
230         self.plugin.command_stack.save(params['output'])
231
232     def _setup_params(self, hooke, params):
233         if params['output'] == None and self.plugin.command_stack.path == None:
234             params['output'] = 'default'
235         if params['output'] != None:
236             params['output'] = os.path.join(
237                 os.path.expanduser(self.plugin.config['path']),
238                 params['output'])
239         return params
240
241 class LoadCommand (CommandStackCommand):
242     """Load the command stack.
243
244     .. warning:: This is *not safe* with untrusted input.
245     """
246     def __init__(self, plugin):
247         super(LoadCommand, self).__init__(
248             name='load command stack',
249             arguments=[
250                 Argument(name='input', type='file',
251                          help="""
252 File name for the input command stack.
253 """.strip()),
254                 ],
255             help=self.__doc__, plugin=plugin)
256
257     def _run(self, hooke, inqueue, outqueue, params):
258         params = self._setup_params(hooke, params)
259         self.plugin.command_stack.clear()
260         self.plugin.command_stack.load(params['input'])
261
262     def _setup_params(self, hooke, params):
263         if params['input'] == None and self.plugin.command_stack.path == None:
264             params['input'] = 'default'
265         if params['input'] != None:
266             params['input'] = os.path.join(
267                 os.path.expanduser(self.plugin.config['path']),
268                 params['input'])
269         return params
270
271
272 class ExecuteCommand (Command):
273     """Execute a :class:`~hooke.command_stack.CommandStack`.
274     """
275     def __init__(self, plugin):
276         super(ExecuteCommand, self).__init__(
277             name='execute command stack',
278             arguments=[
279                 Argument(name='commands', type='command stack',
280                          help="""
281 Command stack to apply to each curve.  Defaults to the plugin's
282 current stack.
283 """.strip()),
284                 ],
285             help=self.__doc__, plugin=plugin)
286
287     def _run(self, hooke, inqueue, outqueue, params):
288         params = self._setup_params(hooke=hooke, params=params)
289         if len(params['commands']) == 0:
290             return
291         params['commands'].execute(hooke=hooke, stack=params['stack'])
292
293     def _setup_params(self, hooke, params):
294         if params['commands'] == None:
295             params['commands'] = self.plugin.command_stack
296         return params