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