1# -*- coding: utf-8 -*-
2#
3#  __init__.py - commander
4#
5#  Copyright (C) 2010 - Jesse van den Kieboom
6#
7#  This program is free software; you can redistribute it and/or modify
8#  it under the terms of the GNU General Public License as published by
9#  the Free Software Foundation; either version 2 of the License, or
10#  (at your option) any later version.
11#
12#  This program is distributed in the hope that it will be useful,
13#  but WITHOUT ANY WARRANTY; without even the implied warranty of
14#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15#  GNU General Public License for more details.
16#
17#  You should have received a copy of the GNU General Public License
18#  along with this program; if not, write to the Free Software
19#  Foundation, Inc., 51 Franklin Street, Fifth Floor,
20#  Boston, MA 02110-1301, USA.
21
22from gi.repository import GLib, GObject, Gio
23
24import sys, bisect, types, shlex, re, os, traceback
25
26from . import module, method, result, exceptions, metamodule, completion
27
28from commands.accel_group import AccelGroup
29from commands.accel_group import Accelerator
30
31__all__ = ['is_commander_module', 'Commands', 'Accelerator']
32
33import commander.modules
34
35def attrs(**kwargs):
36    def generator(f):
37        for k in kwargs:
38            setattr(f, k, kwargs[k])
39
40        return f
41
42    return generator
43
44def autocomplete(d={}, **kwargs):
45    ret = {}
46
47    for dic in (d, kwargs):
48        for k in dic:
49            if type(dic[k]) == types.FunctionType:
50                ret[k] = dic[k]
51
52    return attrs(autocomplete=ret)
53
54def accelerator(*args, **kwargs):
55    return attrs(accelerator=Accelerator(args, kwargs))
56
57def is_commander_module(mod):
58    if type(mod) == types.ModuleType:
59        return mod and ('__commander_module__' in mod.__dict__)
60    else:
61        mod = str(mod)
62        return mod.endswith('.py') or (os.path.isdir(mod) and os.path.isfile(os.path.join(mod, '__init__.py')))
63
64class Singleton(object):
65    _instance = None
66
67    def __new__(cls, *args, **kwargs):
68        if not cls._instance:
69            cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
70            cls._instance.__init_once__()
71
72        return cls._instance
73
74class Commands(Singleton):
75    class Continuated:
76        def __init__(self, generator):
77            self.generator = generator
78            self.retval = None
79
80        def autocomplete_func(self):
81            mod = sys.modules['commander.commands.result']
82
83            if self.retval == mod.Result.PROMPT:
84                return self.retval.autocomplete
85            else:
86                return {}
87
88        def args(self):
89            return [], True
90
91    class State:
92        def __init__(self):
93            self.clear()
94
95        def clear(self):
96            self.stack = []
97
98        def top(self):
99            return self.stack[0]
100
101        def run(self, ret):
102            ct = self.top()
103
104            if ret:
105                ct.retval = ct.generator.send(ret)
106            else:
107                ct.retval = next(ct.generator)
108
109            return ct.retval
110
111        def push(self, gen):
112            self.stack.insert(0, Commands.Continuated(gen))
113
114        def pop(self):
115            if not self.stack:
116                return
117
118            try:
119                self.stack[0].generator.close()
120            except GeneratorExit:
121                pass
122
123            del self.stack[0]
124
125        def __len__(self):
126            return len(self.stack)
127
128        def __nonzero__(self):
129            return len(self) != 0
130
131    def __init_once__(self):
132        self._modules = None
133        self._dirs = []
134        self._monitors = []
135        self._accel_group = None
136
137        self._timeouts = {}
138
139        self._stack = []
140
141    def set_dirs(self, dirs):
142        self._dirs = dirs
143
144    def stop(self):
145        for mon in self._monitors:
146            mon.cancel()
147
148        self._monitors = []
149        self._modules = None
150
151        for k in self._timeouts:
152            GLib.source_remove(self._timeouts[k])
153
154        self._timeouts = {}
155
156    def accelerator_activated(self, accel, mod, state, entry):
157        self.run(state, mod.execute('', [], entry, 0, accel.arguments))
158
159    def scan_accelerators(self, modules=None):
160        if modules == None:
161            self._accel_group = AccelGroup()
162            modules = self.modules()
163
164        recurse_mods = []
165
166        for mod in modules:
167            if type(mod) == types.ModuleType:
168                recurse_mods.append(mod)
169            else:
170                accel = mod.accelerator()
171
172                if accel != None:
173                    self._accel_group.add(accel, self.accelerator_activated, mod)
174
175        for mod in recurse_mods:
176            self.scan_accelerators(mod.commands())
177
178    def accelerator_group(self):
179        if not self._accel_group:
180            self.scan_accelerators()
181
182        return self._accel_group
183
184    def modules(self):
185        self.ensure()
186        return list(self._modules)
187
188    def add_monitor(self, d):
189        gfile = Gio.file_new_for_path(d)
190        monitor = None
191
192        try:
193            monitor = gfile.monitor_directory(Gio.FileMonitorFlags.NONE, None)
194        except Exception as e:
195            # Could not create monitor, this happens on systems where file monitoring is
196            # not supported, but we don't really care
197            pass
198
199        if monitor:
200            monitor.connect('changed', self.on_monitor_changed)
201            self._monitors.append(monitor)
202
203    def scan(self, d):
204        files = []
205
206        try:
207            files = os.listdir(d)
208        except OSError:
209            pass
210
211        for f in files:
212            full = os.path.join(d, f)
213
214            # Test for python files or modules
215            if is_commander_module(full):
216                if self.add_module(full) and os.path.isdir(full):
217                    # Add monitor on the module directory if module was
218                    # successfully added. TODO: recursively add monitors
219                    self.add_monitor(full)
220
221        # Add a monitor on the scanned directory itself
222        self.add_monitor(d)
223
224    def module_name(self, filename):
225        # Module name is the basename without the .py
226        return os.path.basename(os.path.splitext(filename)[0])
227
228    def add_module(self, filename):
229        base = self.module_name(filename)
230
231        # Check if module already exists
232        if base in self._modules:
233            return
234
235        # Create new 'empty' module
236        mod = module.Module(base, os.path.dirname(filename))
237        bisect.insort_right(self._modules, mod)
238
239        # Reload the module
240        self.reload_module(mod)
241        return True
242
243    def ensure(self):
244        # Ensure that modules have been scanned
245        if self._modules != None:
246            return
247
248        self._modules = []
249
250        for d in self._dirs:
251            self.scan(d)
252
253    def _run_generator(self, state, ret=None):
254        mod = sys.modules['commander.commands.result']
255
256        try:
257            # Determine first use
258            retval = state.run(ret)
259
260            if not retval or (isinstance(retval, mod.Result) and (retval == mod.DONE or retval == mod.HIDE)):
261                state.pop()
262
263                if state:
264                    return self._run_generator(state)
265
266            return self.run(state, retval)
267
268        except StopIteration:
269            state.pop()
270
271            if state:
272                return self.run(state)
273        except Exception as e:
274            # Something error like, we throw on the parent generator
275            state.pop()
276
277            if state:
278                state.top().generator.throw(type(e), e, e.__traceback__)
279            else:
280                # Re raise it for the top most to show the error
281                raise
282
283        return None
284
285    def run(self, state, ret=None):
286        mod = sys.modules['commander.commands.result']
287
288        if type(ret) == types.GeneratorType:
289            # Ok, this is cool stuff, generators can ask and susped execution
290            # of commands, for instance to prompt for some more information
291            state.push(ret)
292
293            return self._run_generator(state)
294        elif not isinstance(ret, mod.Result) and len(state) > 1:
295            # Basicly, send it to the previous?
296            state.pop()
297
298            return self._run_generator(state, ret)
299        else:
300            return ret
301
302    def execute(self, state, argstr, words, wordsstr, entry, modifier):
303        self.ensure()
304
305        if state:
306            return self._run_generator(state, [argstr, words, modifier])
307
308        cmd = completion.single_command(wordsstr, 0)
309
310        if not cmd:
311            raise exceptions.Execute('Could not find command: ' + wordsstr[0])
312
313        if len(words) > 1:
314            argstr = argstr[words[1].start(0):]
315        else:
316            argstr = ''
317
318        # Execute command
319        return self.run(state, cmd.execute(argstr, wordsstr[1:], entry, modifier))
320
321    def invoke(self, entry, modifier, command, args, argstr=None):
322        self.ensure()
323
324        cmd = completion.single_command([command], 0)
325
326        if not cmd:
327            raise exceptions.Execute('Could not find command: ' + command)
328
329        if argstr == None:
330            argstr = ' '.join(args)
331
332        ret = cmd.execute(argstr, args, entry, modifier)
333
334        if type(ret) == types.GeneratorType:
335            raise exceptions.Execute('Cannot invoke commands that yield (yet)')
336        else:
337            return ret
338
339    def resolve_module(self, path, load=True):
340        if not self._modules or not is_commander_module(path):
341            return None
342
343        # Strip off __init__.py for module kind of modules
344        if path.endswith('__init__.py'):
345            path = os.path.dirname(path)
346
347        base = self.module_name(path)
348
349        # Find module
350        idx = bisect.bisect_left(self._modules, base)
351        mod = None
352
353        if idx < len(self._modules):
354            mod = self._modules[idx]
355
356        if not mod or mod.name != base:
357            if load:
358                self.add_module(path)
359
360            return None
361
362        return mod
363
364    def remove_module_accelerators(self, modules):
365        recurse_mods = []
366
367        for mod in modules:
368            if type(mod) == types.ModuleType:
369                recurse_mods.append(mod)
370            else:
371                accel = mod.accelerator()
372
373                if accel != None:
374                    self._accel_group.remove(accel)
375
376        for mod in recurse_mods:
377            self.remove_module_accelerators(mod.commands())
378
379    def remove_module(self, mod):
380        # Remove roots
381        for r in mod.roots():
382            if r in self._modules:
383                self._modules.remove(r)
384
385        # Remove accelerators
386        if self._accel_group:
387            self.remove_module_accelerators([mod])
388
389        if mod.name in commander.modules.__dict__:
390            del commander.modules.__dict__[mod.name]
391
392    def reload_module(self, mod):
393        if isinstance(mod, str):
394            mod = self.resolve_module(mod)
395
396        if not mod or not self._modules:
397            return
398
399        # Remove roots
400        self.remove_module(mod)
401
402        # Now, try to reload the module
403        try:
404            mod.reload()
405        except Exception as e:
406            # Reload failed, we remove the module
407            info = traceback.format_tb(sys.exc_info()[2])
408            print('Failed to reload module ({0}):\n  {1}'.format(mod.name, info[-1]))
409
410            self._modules.remove(mod)
411            return
412
413        # Insert roots
414        for r in mod.roots():
415            bisect.insort(self._modules, r)
416
417        commander.modules.__dict__[mod.name] = metamodule.MetaModule(mod.mod)
418
419        if self._accel_group:
420            self.scan_accelerators([mod])
421
422    def on_timeout_delete(self, path, mod):
423        if not path in self._timeouts:
424            return False
425
426        # Remove the module
427        mod.unload()
428        self.remove_module(mod)
429        self._modules.remove(mod)
430
431        return False
432
433    def on_monitor_changed(self, monitor, gfile1, gfile2, evnt):
434        if evnt == Gio.FileMonitorEvent.CHANGED:
435            # Reload the module
436            self.reload_module(gfile1.get_path())
437        elif evnt == Gio.FileMonitorEvent.DELETED:
438            path = gfile1.get_path()
439            mod = self.resolve_module(path, False)
440
441            if not mod:
442                return
443
444            if path in self._timeouts:
445                GLib.source_remove(self._timeouts[path])
446
447            # We add a timeout because a common save strategy causes a
448            # DELETE/CREATE event chain
449            self._timeouts[path] = GLib.timeout_add(500, self.on_timeout_delete, path, mod)
450        elif evnt == Gio.FileMonitorEvent.CREATED:
451            path = gfile1.get_path()
452
453            # Check if this CREATE followed a previous DELETE
454            if path in self._timeouts:
455                GLib.source_remove(self._timeouts[path])
456                del self._timeouts[path]
457
458            # Reload the module
459            self.reload_module(path)
460
461# ex:ts=4:et
462