1# Copyright (C) 2006 - Steve Frécinaux
2#            2016-17 - Nick Boultbee
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8
9# Parts from "Interactive Python-GTK Console"
10# (stolen from epiphany's console.py)
11#     Copyright (C), 1998 James Henstridge <james@daa.com.au>
12#     Copyright (C), 2005 Adam Hooper <adamh@densi.com>
13# Bits from gedit Python Console Plugin
14#     Copyright (C), 2005 Raphaël Slinckx
15
16# PythonConsole taken from totem
17# Plugin parts:
18# Copyright 2009,2010,2013 Christoph Reiter
19#                     2016 Nick Boultbee
20
21
22import sys
23import re
24import traceback
25
26from gi.repository import Gtk, Pango, Gdk, GLib
27
28from quodlibet import _, app, ngettext
29from quodlibet import const
30from quodlibet.plugins.events import EventPlugin
31from quodlibet.plugins.gui import UserInterfacePlugin
32from quodlibet.qltk import Icons, add_css, Align
33from quodlibet.plugins.songsmenu import SongsMenuPlugin
34from quodlibet.util.collection import Collection
35from quodlibet.util import print_
36
37
38class PyConsole(SongsMenuPlugin):
39    PLUGIN_ID = 'Python Console'
40    PLUGIN_NAME = _('Python Console')
41    PLUGIN_DESC = _('Interactive Python console. Opens a new window.')
42    PLUGIN_ICON = Icons.UTILITIES_TERMINAL
43
44    def plugin_songs(self, songs):
45        desc = ngettext("%d song", "%d songs", len(songs)) % len(songs)
46        win = ConsoleWindow(create_console(songs), title=desc)
47        win.set_icon_name(self.PLUGIN_ICON)
48        win.set_title(_("{plugin_name} for {songs} ({app})").format(
49            plugin_name=self.PLUGIN_NAME, songs=desc, app=app.name))
50        win.show_all()
51
52
53class PyConsoleSidebar(EventPlugin, UserInterfacePlugin):
54    PLUGIN_ID = 'Python Console Sidebar'
55    PLUGIN_NAME = _('Python Console Sidebar')
56    PLUGIN_DESC = _('Interactive Python console sidebar, '
57                    'that follows the selected songs in the main window.')
58    PLUGIN_ICON = Icons.UTILITIES_TERMINAL
59
60    def enabled(self):
61        self.console = create_console()
62
63    def plugin_on_songs_selected(self, songs):
64        self.console.namespace = namespace_for(songs)
65
66    def create_sidebar(self):
67        align = Align(self.console)
68        self.sidebar = align
69        self.sidebar.show_all()
70        return align
71
72
73def create_console(songs=None):
74    console = PythonConsole(namespace_for(songs)) if songs else PythonConsole()
75    access_string = _("You can access the following objects by default:")
76    access_string += "\\n".join([
77                    "",
78                    "  %5s: SongWrapper objects",
79                    "  %5s: Song dictionaries",
80                    "  %5s: Filename list",
81                    "  %5s: Songs Collection",
82                    "  %5s: Application instance"]) % (
83                       "songs", "sdict", "files", "col", "app")
84
85    dir_string = _("Your current working directory is:")
86
87    console.eval("import mutagen", False)
88    console.eval("import os", False)
89    console.eval("print(\"Python: %s / Quod Libet: %s\")" %
90                 (sys.version.split()[0], const.VERSION), False)
91    console.eval("print(\"%s\")" % access_string, False)
92    console.eval("print(\"%s \"+ os.getcwd())" % dir_string, False)
93    return console
94
95
96def namespace_for(song_wrappers):
97    files = [song('~filename') for song in song_wrappers]
98    song_dicts = [song._song for song in song_wrappers]
99    collection = Collection()
100    collection.songs = song_dicts
101    return {
102        'songs': song_wrappers,
103        'files': files,
104        'sdict': song_dicts,
105        'col': collection,
106        'app': app}
107
108
109class ConsoleWindow(Gtk.Window):
110    def __init__(self, console, title=None):
111        Gtk.Window.__init__(self)
112        if title:
113            self.set_title(title)
114        self.add(console)
115        self.set_size_request(700, 500)
116        console.connect("destroy", lambda *x: self.destroy())
117
118
119class PythonConsole(Gtk.ScrolledWindow):
120    def __init__(self, namespace=None, destroy_cb=None):
121        Gtk.ScrolledWindow.__init__(self)
122
123        self.destroy_cb = destroy_cb
124        self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
125        self.set_shadow_type(Gtk.ShadowType.NONE)
126        self.view = Gtk.TextView()
127        add_css(self, "* { background-color: white; padding: 6px; } ")
128        self.view.modify_font(Pango.font_description_from_string('Monospace'))
129        self.view.set_editable(True)
130        self.view.set_wrap_mode(Gtk.WrapMode.CHAR)
131        self.add(self.view)
132        self.view.show()
133
134        buffer = self.view.get_buffer()
135        self.normal = buffer.create_tag("normal")
136        self.error = buffer.create_tag("error")
137        self.error.set_property("foreground", "red")
138        self.command = buffer.create_tag("command")
139        self.command.set_property("foreground", "blue")
140
141        self.__spaces_pattern = re.compile(r'^\s+')
142        self.namespace = namespace or {}
143
144        self.block_command = False
145
146        # Init first line
147        buffer.create_mark("input-line", buffer.get_end_iter(), True)
148        buffer.insert(buffer.get_end_iter(), ">>> ")
149        buffer.create_mark("input", buffer.get_end_iter(), True)
150
151        # Init history
152        self.history = ['']
153        self.history_pos = 0
154        self.current_command = ''
155        self.namespace['__history__'] = self.history
156
157        # Set up hooks for standard output.
158        self.stdout = OutFile(self, self.normal)
159        self.stderr = OutFile(self, self.error)
160
161        # Signals
162        self.view.connect("key-press-event", self.__key_press_event_cb)
163        buffer.connect("mark-set", self.__mark_set_cb)
164
165    def __key_press_event_cb(self, view, event):
166        modifier_mask = Gtk.accelerator_get_default_mod_mask()
167        event_state = event.state & modifier_mask
168
169        if event.keyval == Gdk.KEY_d and \
170                        event_state == Gdk.ModifierType.CONTROL_MASK:
171            self.destroy()
172
173        elif event.keyval == Gdk.KEY_Return and \
174                        event_state == Gdk.ModifierType.CONTROL_MASK:
175            # Get the command
176            buffer = view.get_buffer()
177            inp_mark = buffer.get_mark("input")
178            inp = buffer.get_iter_at_mark(inp_mark)
179            cur = buffer.get_end_iter()
180            line = buffer.get_text(inp, cur, True)
181            self.current_command = self.current_command + line + "\n"
182            self.history_add(line)
183
184            # Prepare the new line
185            cur = buffer.get_end_iter()
186            buffer.insert(cur, "\n... ")
187            cur = buffer.get_end_iter()
188            buffer.move_mark(inp_mark, cur)
189
190            # Keep indentation of preceding line
191            spaces = re.match(self.__spaces_pattern, line)
192            if spaces is not None:
193                buffer.insert(cur, line[spaces.start():spaces.end()])
194                cur = buffer.get_end_iter()
195
196            buffer.place_cursor(cur)
197            GLib.idle_add(self.scroll_to_end)
198            return True
199
200        elif event.keyval == Gdk.KEY_Return:
201            # Get the marks
202            buffer = view.get_buffer()
203            lin_mark = buffer.get_mark("input-line")
204            inp_mark = buffer.get_mark("input")
205
206            # Get the command line
207            inp = buffer.get_iter_at_mark(inp_mark)
208            cur = buffer.get_end_iter()
209            line = buffer.get_text(inp, cur, True)
210            self.current_command = self.current_command + line + "\n"
211            self.history_add(line)
212
213            # Make the line blue
214            lin = buffer.get_iter_at_mark(lin_mark)
215            buffer.apply_tag(self.command, lin, cur)
216            buffer.insert(cur, "\n")
217
218            cur_strip = self.current_command.rstrip()
219
220            if (cur_strip.endswith(":") or
221                (self.current_command[-2:] != "\n\n" and self.block_command)):
222                # Unfinished block command
223                self.block_command = True
224                com_mark = "... "
225            elif cur_strip.endswith("\\"):
226                com_mark = "... "
227            else:
228                # Eval the command
229                self.__run(self.current_command)
230                self.current_command = ''
231                self.block_command = False
232                com_mark = ">>> "
233
234            # Prepare the new line
235            cur = buffer.get_end_iter()
236            buffer.move_mark(lin_mark, cur)
237            buffer.insert(cur, com_mark)
238            cur = buffer.get_end_iter()
239            buffer.move_mark(inp_mark, cur)
240            buffer.place_cursor(cur)
241            GLib.idle_add(self.scroll_to_end)
242            return True
243
244        elif event.keyval == Gdk.KEY_KP_Down or event.keyval == Gdk.KEY_Down:
245            # Next entry from history
246            view.emit_stop_by_name("key_press_event")
247            self.history_down()
248            GLib.idle_add(self.scroll_to_end)
249            return True
250
251        elif event.keyval == Gdk.KEY_KP_Up or event.keyval == Gdk.KEY_Up:
252            # Previous entry from history
253            view.emit_stop_by_name("key_press_event")
254            self.history_up()
255            GLib.idle_add(self.scroll_to_end)
256            return True
257
258        elif event.keyval == Gdk.KEY_KP_Left or \
259                        event.keyval == Gdk.KEY_Left or \
260                        event.keyval == Gdk.KEY_BackSpace:
261            buffer = view.get_buffer()
262            inp = buffer.get_iter_at_mark(buffer.get_mark("input"))
263            cur = buffer.get_iter_at_mark(buffer.get_insert())
264            return inp.compare(cur) == 0
265
266        elif event.keyval == Gdk.KEY_Home:
267            # Go to the begin of the command instead of the begin of the line
268            buffer = view.get_buffer()
269            inp = buffer.get_iter_at_mark(buffer.get_mark("input"))
270            if event_state == Gdk.ModifierType.SHIFT_MASK:
271                buffer.move_mark_by_name("insert", inp)
272            else:
273                buffer.place_cursor(inp)
274            return True
275
276    def __mark_set_cb(self, buffer, iter, name):
277        input = buffer.get_iter_at_mark(buffer.get_mark("input"))
278        pos = buffer.get_iter_at_mark(buffer.get_insert())
279        self.view.set_editable(pos.compare(input) != -1)
280
281    def get_command_line(self):
282        buffer = self.view.get_buffer()
283        inp = buffer.get_iter_at_mark(buffer.get_mark("input"))
284        cur = buffer.get_end_iter()
285        return buffer.get_text(inp, cur, True)
286
287    def set_command_line(self, command):
288        buffer = self.view.get_buffer()
289        mark = buffer.get_mark("input")
290        inp = buffer.get_iter_at_mark(mark)
291        cur = buffer.get_end_iter()
292        buffer.delete(inp, cur)
293        buffer.insert(inp, command)
294        buffer.select_range(buffer.get_iter_at_mark(mark),
295                            buffer.get_end_iter())
296        self.view.grab_focus()
297
298    def history_add(self, line):
299        if line.strip() != '':
300            self.history_pos = len(self.history)
301            self.history[self.history_pos - 1] = line
302            self.history.append('')
303
304    def history_up(self):
305        if self.history_pos > 0:
306            self.history[self.history_pos] = self.get_command_line()
307            self.history_pos -= 1
308            self.set_command_line(self.history[self.history_pos])
309
310    def history_down(self):
311        if self.history_pos < len(self.history) - 1:
312            self.history[self.history_pos] = self.get_command_line()
313            self.history_pos += 1
314            self.set_command_line(self.history[self.history_pos])
315
316    def scroll_to_end(self):
317        iter = self.view.get_buffer().get_end_iter()
318        self.view.scroll_to_iter(iter, 0.0, False, 0.5, 0.5)
319        return False
320
321    def write(self, text, tag=None):
322        buf = self.view.get_buffer()
323        if tag is None:
324            buf.insert(buf.get_end_iter(), text)
325        else:
326            buf.insert_with_tags(buf.get_end_iter(), text, tag)
327
328        GLib.idle_add(self.scroll_to_end)
329
330    def eval(self, command, display_command=False):
331        buffer = self.view.get_buffer()
332        lin = buffer.get_mark("input-line")
333        buffer.delete(buffer.get_iter_at_mark(lin),
334                      buffer.get_end_iter())
335
336        if isinstance(command, list) or isinstance(command, tuple):
337            for c in command:
338                if display_command:
339                    self.write(">>> " + c + "\n", self.command)
340                self.__run(c)
341        else:
342            if display_command:
343                self.write(">>> " + c + "\n", self.command)
344            self.__run(command)
345
346        cur = buffer.get_end_iter()
347        buffer.move_mark_by_name("input-line", cur)
348        buffer.insert(cur, ">>> ")
349        cur = buffer.get_end_iter()
350        buffer.move_mark_by_name("input", cur)
351        self.view.scroll_to_iter(buffer.get_end_iter(), 0.0, False, 0.5, 0.5)
352
353    def __run(self, command):
354        sys.stdout, self.stdout = self.stdout, sys.stdout
355        sys.stderr, self.stderr = self.stderr, sys.stderr
356
357        try:
358            try:
359                r = eval(command, self.namespace, self.namespace)
360                if r is not None:
361                    print_(repr(r))
362            except SyntaxError:
363                exec(command, self.namespace)
364        except:
365            if hasattr(sys, 'last_type') and sys.last_type == SystemExit:
366                self.destroy()
367            else:
368                traceback.print_exc()
369
370        sys.stdout, self.stdout = self.stdout, sys.stdout
371        sys.stderr, self.stderr = self.stderr, sys.stderr
372
373
374class OutFile(object):
375    """A fake output file object. It sends output to a TK test widget,
376    and if asked for a file number, returns one set on instance creation"""
377
378    def __init__(self, console, tag):
379        self.console = console
380        self.tag = tag
381
382    def close(self):
383        pass
384
385    def flush(self):
386        pass
387
388    def fileno(self):
389        raise IOError
390
391    def isatty(self):
392        return 0
393
394    def read(self, a):
395        return ''
396
397    def readline(self):
398        return ''
399
400    def readlines(self):
401        return []
402
403    def write(self, s):
404        self.console.write(s, self.tag)
405
406    def writelines(self, l):
407        self.console.write(l, self.tag)
408
409    def seek(self, a):
410        raise IOError(29, 'Illegal seek')
411
412    def tell(self):
413        raise IOError(29, 'Illegal seek')
414
415    truncate = tell
416