console.py (11471B)
1 2 # source: http://stackoverflow.com/questions/2758159/how-to-embed-a-python-interpreter-in-a-pyqt-widget 3 4 import sys 5 import os 6 import re 7 import traceback 8 9 from PyQt5 import QtCore 10 from PyQt5 import QtGui 11 from PyQt5 import QtWidgets 12 13 from electrum import util 14 from electrum.i18n import _ 15 16 from .util import MONOSPACE_FONT 17 18 # sys.ps1 and sys.ps2 are only declared if an interpreter is in interactive mode. 19 sys.ps1 = '>>> ' 20 sys.ps2 = '... ' 21 22 23 class OverlayLabel(QtWidgets.QLabel): 24 STYLESHEET = ''' 25 QLabel, QLabel link { 26 color: rgb(0, 0, 0); 27 background-color: rgb(248, 240, 200); 28 border: 1px solid; 29 border-color: rgb(255, 114, 47); 30 padding: 2px; 31 } 32 ''' 33 def __init__(self, text, parent): 34 super().__init__(text, parent) 35 self.setMinimumHeight(150) 36 self.setGeometry(0, 0, self.width(), self.height()) 37 self.setStyleSheet(self.STYLESHEET) 38 self.setMargin(0) 39 parent.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 40 self.setWordWrap(True) 41 42 def mousePressEvent(self, e): 43 self.hide() 44 45 def on_resize(self, w): 46 padding = 2 # px, from the stylesheet above 47 self.setFixedWidth(w - padding) 48 49 50 class Console(QtWidgets.QPlainTextEdit): 51 def __init__(self, parent=None): 52 QtWidgets.QPlainTextEdit.__init__(self, parent) 53 54 self.history = [] 55 self.namespace = {} 56 self.construct = [] 57 58 self.setGeometry(50, 75, 600, 400) 59 self.setWordWrapMode(QtGui.QTextOption.WrapAnywhere) 60 self.setUndoRedoEnabled(False) 61 self.document().setDefaultFont(QtGui.QFont(MONOSPACE_FONT, 10, QtGui.QFont.Normal)) 62 self.newPrompt("") # make sure there is always a prompt, even before first server.banner 63 64 self.updateNamespace({'run':self.run_script}) 65 self.set_json(False) 66 67 warning_text = "<h1>{}</h1><br>{}<br><br>{}".format( 68 _("Warning!"), 69 _("Do not paste code here that you don't understand. Executing the wrong code could lead " 70 "to your coins being irreversibly lost."), 71 _("Click here to hide this message.") 72 ) 73 self.messageOverlay = OverlayLabel(warning_text, self) 74 75 def resizeEvent(self, e): 76 super().resizeEvent(e) 77 vertical_scrollbar_width = self.verticalScrollBar().width() * self.verticalScrollBar().isVisible() 78 self.messageOverlay.on_resize(self.width() - vertical_scrollbar_width) 79 80 def set_json(self, b): 81 self.is_json = b 82 83 def run_script(self, filename): 84 with open(filename) as f: 85 script = f.read() 86 87 self.exec_command(script) 88 89 def updateNamespace(self, namespace): 90 self.namespace.update(namespace) 91 92 def showMessage(self, message): 93 curr_line = self.getCommand(strip=False) 94 self.appendPlainText(message) 95 self.newPrompt(curr_line) 96 97 def clear(self): 98 curr_line = self.getCommand() 99 self.setPlainText('') 100 self.newPrompt(curr_line) 101 102 def keyboard_interrupt(self): 103 self.construct = [] 104 self.appendPlainText('KeyboardInterrupt') 105 self.newPrompt('') 106 107 def newPrompt(self, curr_line): 108 if self.construct: 109 prompt = sys.ps2 + curr_line 110 else: 111 prompt = sys.ps1 + curr_line 112 113 self.completions_pos = self.textCursor().position() 114 self.completions_visible = False 115 116 self.appendPlainText(prompt) 117 self.moveCursor(QtGui.QTextCursor.End) 118 119 def getCommand(self, *, strip=True): 120 doc = self.document() 121 curr_line = doc.findBlockByLineNumber(doc.lineCount() - 1).text() 122 if strip: 123 curr_line = curr_line.rstrip() 124 curr_line = curr_line[len(sys.ps1):] 125 return curr_line 126 127 def setCommand(self, command): 128 if self.getCommand() == command: 129 return 130 131 doc = self.document() 132 curr_line = doc.findBlockByLineNumber(doc.lineCount() - 1).text() 133 self.moveCursor(QtGui.QTextCursor.End) 134 for i in range(len(curr_line) - len(sys.ps1)): 135 self.moveCursor(QtGui.QTextCursor.Left, QtGui.QTextCursor.KeepAnchor) 136 137 self.textCursor().removeSelectedText() 138 self.textCursor().insertText(command) 139 self.moveCursor(QtGui.QTextCursor.End) 140 141 def show_completions(self, completions): 142 if self.completions_visible: 143 self.hide_completions() 144 145 c = self.textCursor() 146 c.setPosition(self.completions_pos) 147 148 completions = map(lambda x: x.split('.')[-1], completions) 149 t = '\n' + ' '.join(completions) 150 if len(t) > 500: 151 t = t[:500] + '...' 152 c.insertText(t) 153 self.completions_end = c.position() 154 155 self.moveCursor(QtGui.QTextCursor.End) 156 self.completions_visible = True 157 158 def hide_completions(self): 159 if not self.completions_visible: 160 return 161 c = self.textCursor() 162 c.setPosition(self.completions_pos) 163 l = self.completions_end - self.completions_pos 164 for x in range(l): c.deleteChar() 165 166 self.moveCursor(QtGui.QTextCursor.End) 167 self.completions_visible = False 168 169 def getConstruct(self, command): 170 if self.construct: 171 self.construct.append(command) 172 if not command: 173 ret_val = '\n'.join(self.construct) 174 self.construct = [] 175 return ret_val 176 else: 177 return '' 178 else: 179 if command and command[-1] == (':'): 180 self.construct.append(command) 181 return '' 182 else: 183 return command 184 185 def addToHistory(self, command): 186 if not self.construct and command[0:1] == ' ': 187 return 188 189 if command and (not self.history or self.history[-1] != command): 190 self.history.append(command) 191 self.history_index = len(self.history) 192 193 def getPrevHistoryEntry(self): 194 if self.history: 195 self.history_index = max(0, self.history_index - 1) 196 return self.history[self.history_index] 197 return '' 198 199 def getNextHistoryEntry(self): 200 if self.history: 201 hist_len = len(self.history) 202 self.history_index = min(hist_len, self.history_index + 1) 203 if self.history_index < hist_len: 204 return self.history[self.history_index] 205 return '' 206 207 def getCursorPosition(self): 208 c = self.textCursor() 209 return c.position() - c.block().position() - len(sys.ps1) 210 211 def setCursorPosition(self, position): 212 self.moveCursor(QtGui.QTextCursor.StartOfLine) 213 for i in range(len(sys.ps1) + position): 214 self.moveCursor(QtGui.QTextCursor.Right) 215 216 def run_command(self): 217 command = self.getCommand() 218 self.addToHistory(command) 219 220 command = self.getConstruct(command) 221 222 if command: 223 self.exec_command(command) 224 self.newPrompt('') 225 self.set_json(False) 226 227 def exec_command(self, command): 228 tmp_stdout = sys.stdout 229 230 class stdoutProxy(): 231 def __init__(self, write_func): 232 self.write_func = write_func 233 self.skip = False 234 235 def flush(self): 236 pass 237 238 def write(self, text): 239 if not self.skip: 240 stripped_text = text.rstrip('\n') 241 self.write_func(stripped_text) 242 QtCore.QCoreApplication.processEvents() 243 self.skip = not self.skip 244 245 if type(self.namespace.get(command)) == type(lambda:None): 246 self.appendPlainText("'{}' is a function. Type '{}()' to use it in the Python console." 247 .format(command, command)) 248 return 249 250 sys.stdout = stdoutProxy(self.appendPlainText) 251 try: 252 try: 253 # eval is generally considered bad practice. use it wisely! 254 result = eval(command, self.namespace, self.namespace) 255 if result is not None: 256 if self.is_json: 257 util.print_msg(util.json_encode(result)) 258 else: 259 self.appendPlainText(repr(result)) 260 except SyntaxError: 261 # exec is generally considered bad practice. use it wisely! 262 exec(command, self.namespace, self.namespace) 263 except SystemExit: 264 self.close() 265 except BaseException: 266 traceback_lines = traceback.format_exc().split('\n') 267 # Remove traceback mentioning this file, and a linebreak 268 for i in (3,2,1,-1): 269 traceback_lines.pop(i) 270 self.appendPlainText('\n'.join(traceback_lines)) 271 sys.stdout = tmp_stdout 272 273 def keyPressEvent(self, event): 274 if event.key() == QtCore.Qt.Key_Tab: 275 self.completions() 276 return 277 278 self.hide_completions() 279 280 if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return): 281 self.run_command() 282 return 283 if event.key() == QtCore.Qt.Key_Home: 284 self.setCursorPosition(0) 285 return 286 if event.key() == QtCore.Qt.Key_PageUp: 287 return 288 elif event.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Backspace): 289 if self.getCursorPosition() == 0: 290 return 291 elif event.key() == QtCore.Qt.Key_Up: 292 self.setCommand(self.getPrevHistoryEntry()) 293 return 294 elif event.key() == QtCore.Qt.Key_Down: 295 self.setCommand(self.getNextHistoryEntry()) 296 return 297 elif event.key() == QtCore.Qt.Key_L and event.modifiers() == QtCore.Qt.ControlModifier: 298 self.clear() 299 elif event.key() == QtCore.Qt.Key_C and event.modifiers() == QtCore.Qt.ControlModifier: 300 if not self.textCursor().selectedText(): 301 self.keyboard_interrupt() 302 303 super(Console, self).keyPressEvent(event) 304 305 def completions(self): 306 cmd = self.getCommand() 307 # note for regex: new words start after ' ' or '(' or ')' 308 lastword = re.split(r'[ ()]', cmd)[-1] 309 beginning = cmd[0:-len(lastword)] 310 311 path = lastword.split('.') 312 prefix = '.'.join(path[:-1]) 313 prefix = (prefix + '.') if prefix else prefix 314 ns = self.namespace.keys() 315 316 if len(path) == 1: 317 ns = ns 318 else: 319 assert len(path) > 1 320 obj = self.namespace.get(path[0]) 321 try: 322 for attr in path[1:-1]: 323 obj = getattr(obj, attr) 324 except AttributeError: 325 ns = [] 326 else: 327 ns = dir(obj) 328 329 completions = [] 330 for name in ns: 331 if name[0] == '_':continue 332 if name.startswith(path[-1]): 333 completions.append(prefix+name) 334 completions.sort() 335 336 if not completions: 337 self.hide_completions() 338 elif len(completions) == 1: 339 self.hide_completions() 340 self.setCommand(beginning + completions[0]) 341 else: 342 # find common prefix 343 p = os.path.commonprefix(completions) 344 if len(p)>len(lastword): 345 self.hide_completions() 346 self.setCommand(beginning + p) 347 else: 348 self.show_completions(completions)