electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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)