-WIP-electrum-btcp/gui/kivy/console.py

320 lines
9.7 KiB
Python

# source: http://stackoverflow.com/questions/2758159/how-to-embed-a-python-interpreter-in-a-pyqt-widget
import sys, os, re
import traceback, platform
from kivy.core.window import Keyboard
from kivy.uix.textinput import TextInput
from kivy.properties import StringProperty, ListProperty, DictProperty
from kivy.clock import Clock
from electrum import util
if platform.system() == 'Windows':
MONOSPACE_FONT = 'Lucida Console'
elif platform.system() == 'Darwin':
MONOSPACE_FONT = 'Monaco'
else:
MONOSPACE_FONT = 'monospace'
class Console(TextInput):
prompt = StringProperty('>> ')
'''String representing the Prompt message'''
startup_message = StringProperty('')
'''Startup Message to be displayed in the Console if any'''
history = ListProperty([])
'''History of the console'''
namespace = DictProperty({})
'''Dict representing the current namespace of the console'''
def __init__(self, **kwargs):
super(Console, self).__init__(**kwargs)
self.construct = []
self.showMessage(self.startup_message)
self.updateNamespace({'run':self.run_script})
self.set_json(False)
def set_json(self, b):
self.is_json = b
def run_script(self, filename):
with open(filename) as f:
script = f.read()
result = eval(script, self.namespace, self.namespace)
def updateNamespace(self, namespace):
self.namespace.update(namespace)
def showMessage(self, message):
self.appendPlainText(message)
self.newPrompt()
def clear(self):
self.setPlainText('')
self.newPrompt()
def newPrompt(self):
if self.construct:
prompt = '.' * len(self.prompt)
else:
prompt = self.prompt
self.completions_pos = self.cursor_index()
self.completions_visible = False
self.appendPlainText(prompt)
self.move_cursor_to('end')
def getCommand(self):
curr_line = self._lines[-1]
curr_line = curr_line.rstrip()
curr_line = curr_line[len(self.prompt):]
return curr_line
def setCommand(self, command):
if self.getCommand() == command:
return
curr_line = self._lines[-1]
last_pos = len(self.text)
self.select_text(last_pos - len(curr_line) + len(self.prompt), last_pos)
self.delete_selection()
self.insert_text(command)
def show_completions(self, completions):
if self.completions_visible:
self.hide_completions()
self.move_cursor_to(self.completions_pos)
completions = map(lambda x: x.split('.')[-1], completions)
t = '\n' + ' '.join(completions)
if len(t) > 500:
t = t[:500] + '...'
self.insert_text(t)
self.completions_end = self.cursor_index()
self.move_cursor_to('end')
self.completions_visible = True
def hide_completions(self):
if not self.completions_visible:
return
self.move_cursor_to(self.completions_pos)
l = self.completions_end - self.completions_pos
for x in range(l):
self.move_cursor_to('cursor_right')
self.do_backspace()
self.move_cursor_to('end')
self.completions_visible = False
def getConstruct(self, command):
if self.construct:
prev_command = self.construct[-1]
self.construct.append(command)
if not prev_command and not command:
ret_val = '\n'.join(self.construct)
self.construct = []
return ret_val
else:
return ''
else:
if command and command[-1] == (':'):
self.construct.append(command)
return ''
else:
return command
def getHistory(self):
return self.history
def setHisory(self, history):
self.history = history
def addToHistory(self, command):
if command and (not self.history or self.history[-1] != command):
self.history.append(command)
self.history_index = len(self.history)
def getPrevHistoryEntry(self):
if self.history:
self.history_index = max(0, self.history_index - 1)
return self.history[self.history_index]
return ''
def getNextHistoryEntry(self):
if self.history:
hist_len = len(self.history)
self.history_index = min(hist_len, self.history_index + 1)
if self.history_index < hist_len:
return self.history[self.history_index]
return ''
def getCursorPosition(self):
return self.cursor[0] - len(self.prompt)
def setCursorPosition(self, position):
self.cursor = (len(self.prompt) + position, self.cursor[1])
def register_command(self, c, func):
methods = { c: func}
self.updateNamespace(methods)
def runCommand(self):
command = self.getCommand()
self.addToHistory(command)
command = self.getConstruct(command)
if command:
tmp_stdout = sys.stdout
class stdoutProxy():
def __init__(self, write_func):
self.write_func = write_func
self.skip = False
def flush(self):
pass
def write(self, text):
if not self.skip:
stripped_text = text.rstrip('\n')
self.write_func(stripped_text)
self.skip = not self.skip
if type(self.namespace.get(command)) == type(lambda:None):
self.appendPlainText("'%s' is a function. Type '%s()' to use it in the Python console."%(command, command))
self.newPrompt()
return
sys.stdout = stdoutProxy(self.appendPlainText)
try:
try:
result = eval(command, self.namespace, self.namespace)
if result != None:
if self.is_json:
util.print_json(result)
else:
self.appendPlainText(repr(result))
except SyntaxError:
exec command in self.namespace
except SystemExit:
pass
except:
traceback_lines = traceback.format_exc().split('\n')
# Remove traceback mentioning this file, and a linebreak
for i in (3,2,1,-1):
traceback_lines.pop(i)
self.appendPlainText('\n'.join(traceback_lines))
sys.stdout = tmp_stdout
self.newPrompt()
self.set_json(False)
def _keyboard_on_key_down(self, window, keycode, text, modifiers):
self._hide_cut_copy_paste()
is_osx = sys.platform == 'darwin'
# Keycodes on OSX:
ctrl, cmd = 64, 1024
key, key_str = keycode
if key == Keyboard.keycodes['tab']:
self.completions()
return
self.hide_completions()
if key == Keyboard.keycodes['enter']:
self.runCommand()
return
if key == Keyboard.keycodes['home']:
self.setCursorPosition(0)
return
if key == Keyboard.keycodes['pageup']:
return
elif key in (Keyboard.keycodes['left'], Keyboard.keycodes['backspace']):
if self.getCursorPosition() == 0:
return
elif key == Keyboard.keycodes['up']:
self.setCommand(self.getPrevHistoryEntry())
return
elif key == Keyboard.keycodes['down']:
self.setCommand(self.getNextHistoryEntry())
return
elif key == Keyboard.keycodes['l'] and modifiers == ['ctrl']:
self.clear()
super(Console, self)._keyboard_on_key_down(window, keycode, text, modifiers)
def completions(self):
cmd = self.getCommand()
lastword = re.split(' |\(|\)',cmd)[-1]
beginning = cmd[0:-len(lastword)]
path = lastword.split('.')
ns = self.namespace.keys()
if len(path) == 1:
ns = ns
prefix = ''
else:
obj = self.namespace.get(path[0])
prefix = path[0] + '.'
ns = dir(obj)
completions = []
for x in ns:
if x[0] == '_':continue
xx = prefix + x
if xx.startswith(lastword):
completions.append(xx)
completions.sort()
if not completions:
self.hide_completions()
elif len(completions) == 1:
self.hide_completions()
self.setCommand(beginning + completions[0])
else:
# find common prefix
p = os.path.commonprefix(completions)
if len(p)>len(lastword):
self.hide_completions()
self.setCommand(beginning + p)
else:
self.show_completions(completions)
# NEW
def setPlainText(self, message):
"""Equivalent to QT version"""
self.text = message
# NEW
def appendPlainText(self, message):
"""Equivalent to QT version"""
if len(self.text) == 0:
self.text = message
else:
if message:
self.text += '\n' + message
# NEW
def move_cursor_to(self, pos):
"""Aggregate all cursor moving functions"""
if isinstance(pos, int):
self.cursor = self.get_cursor_from_index(pos)
elif pos in ('end', 'pgend', 'pageend'):
def updt_cursor(*l):
self.cursor = self.get_cursor_from_index(self.text)
Clock.schedule_once(updt_cursor)
else: # cursor_home, cursor_end, ... (see docs)
self.do_cursor_movement(pos)