#!/usr/bin/env python
"""
Implements some dialog utilities for tableexplore
Created Feb 2019
Copyright (C) Damien Farrell
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 3
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
import os, sys, platform
import atexit
if platform.system() in ['Linux','Darwin']:
import readline
elif platform.system() == 'Windows':
from pyreadline.rlmain import Readline
readline = Readline()
import rlcompleter
from .qt import *
AUTOCOMPLETE_LIMIT = 20
AUTOCOMPLETE_SEPARATOR = "\n"
style = '''
QPlainTextEdit {
background-color: #20262c;
color: white;
font-family: Consolas, Monaco, monospace;
}
'''
[docs]class QueueReceiver(QtCore.QObject):
sent = QtCore.Signal(str)
def __init__(self, queue, *args, **kwargs):
QtCore.QObject.__init__(self,*args,**kwargs)
self.queue = queue
[docs] @QtCore.Slot()
def run(self):
while True:
text = self.queue.get()
self.sent.emit(text)
[docs]class ExecThread(QtCore.QObject):
finished = QtCore.Signal()
def_to_run = None
cmd = None
[docs] @QtCore.Slot()
def run(self):
try:
self.def_to_run(self.cmd)
except:
(type, value, traceback) = sys.exc_info()
sys.excepthook(type, value, traceback)
self.finished.emit()
[docs]class Terminal(QPlainTextEdit):
# signal to connect at the interpreter run code
press_enter = QtCore.Signal(str)
def __init__(self, parent=None, hist_file=None):
"""
Micmic python terminal interpreter from a QPlainTextEdit. Can be override for app integration.
Readline history is implemented, it's possible to change hist file path by define
Terminal.hist_file attr.
:param parent: parent widget
"""
QPlainTextEdit.__init__(self, parent)
self.setGeometry(50, 75, 600, 400)
#self.setWordWrapMode(QTextOption.WrapAnywhere)
self.setUndoRedoEnabled(False)
font = QFont("Monospace")
font.setPointSize(10)
self.setFont(font)
self.prompt = None
self.cursor_line = None
if hist_file == None:
self.hist_file = os.path.join(os.path.expanduser("~"), ".pyTermHist")
else:
self.hist_file = hist_file
self.init_history(self.hist_file)
self.history_index = readline.get_current_history_length()
self.completer = rlcompleter.Completer()
self.def_to_run_code = None
self.thread = None
self.setTabStopWidth(4)
# connection cursor line position
self.cursorPositionChanged.connect(self.count_cursor_lines)
# connect press enter
self.press_enter.connect(self.exec_code)
self.setStyleSheet(style)
return
[docs] def active_queue_thread(self, queue):
self.thread_q = QtCore.QThread()
self.receiver = QueueReceiver(queue)
self.receiver.sent.connect(self.write)
self.receiver.moveToThread(self.thread_q)
self.thread_q.started.connect(self.receiver.run)
self.thread_q.start()
[docs] def setStyle(self, style='default'):
if style == 'light':
ss = """background-color: #FBFBF8;
color: black;"""
else:
ss = """background-color: #20262c;
color: white;"""
self.setStyleSheet(ss)
return
[docs] def zoom(self, delta):
if delta < 0:
self.zoomOut(1)
elif delta > 0:
self.zoomIn(1)
[docs] def init_history(self, hist_file):
"""
History initialisation with readline GNU, and use hook atexit for save history when program closing.
:param hist_file: history file path
:return:
"""
if hasattr(readline, "read_history_file"):
try:
readline.read_history_file(hist_file)
except IOError:
pass
atexit.register(self.save_history, hist_file)
[docs] def save_history(self, hist_file):
"""
Hook def execute by atexit.
:param hist_file: history file path
:return:
"""
readline.set_history_length(1000)
readline.write_history_file(hist_file)
[docs] def write(self, data):
"""
Append text to the Terminal. And keep cursor at the end.
:param data: str data to write.
:return:
"""
self.appendPlainText(data)
self.moveCursor(QTextCursor.End)
[docs] def write_prompt(self, data):
"""
Append text to the Terminal. And keep cursor at the end.
:param data: str data to write.
:return:
"""
import time
time.sleep(4)
self.appendPlainText(data)
# self.moveCursor(QTextCursor.End)
# self.remove_last_line()
[docs] def get_command(self):
"""
Get command, read last line and remove prompt length.
:return: str command
"""
doc = self.document()
current_line = (doc.findBlockByLineNumber(self.get_last_line() - 1).text())
current_line = current_line.rstrip()
current_line = current_line[len(self.prompt):]
return current_line
[docs] def get_last_line(self):
"""
Get last terminal line.
:return: str last line
"""
doc = self.document()
last_line = doc.lineCount()
return last_line
[docs] def get_cursor_position(self):
"""
Get cursor position
:return: int line cursor position.
"""
return self.textCursor().columnNumber() - len(self.prompt)
[docs] def remove_last_line(self):
# cursor = QTextCursor(self.document().findBlockByLineNumber(self.get_last_line()-5))
cursor = self.textCursor()
cursor.movePosition(QTextCursor.Up)
cursor.movePosition(QTextCursor.Up)
cursor.movePosition(QTextCursor.Up)
cursor.movePosition(QTextCursor.Up)
cursor.movePosition(QTextCursor.Up)
self.setTextCursor(cursor)
[docs] def remove_last_command(self):
"""
Remove current command. Useful for display history navigation.
:return:
"""
cursor = self.textCursor()
cursor.movePosition(QTextCursor.End)
cursor.select(QTextCursor.LineUnderCursor)
cursor.removeSelectedText()
cursor.deletePreviousChar()
self.setTextCursor(cursor)
[docs] def get_previous_history(self):
"""
Get previous history in the readline GNU history file.
:return: str history
"""
self.history_index += 1
if self.history_index >= readline.get_current_history_length():
self.history_index = readline.get_current_history_length()
return readline.get_history_item(self.history_index)
[docs] def get_next_history(self):
"""
Get next history in the readline GNU history file.
:return: str history
"""
if self.history_index <= 1:
self.history_index = 1
hist = readline.get_history_item(self.history_index)
self.history_index -= 1
return hist
[docs] def autocomplete(self, command):
"""
Ask different possibility from command arg, proposition is limited by AUTOCOMPLETE_LIMIT constant
:param command: str
:return: list of proposition
"""
propositions = []
completer = self.completer
for i in range(AUTOCOMPLETE_LIMIT):
ret = completer.complete(command, i)
if ret:
propositions.append(ret)
else:
break
return propositions
[docs] def write_autocomplete(self, command):
"""
Prepare text to write.
:param command: str command
:return:
"""
# Is = or space inside ?
text_after_eq = command.split("=")[-1]
text_strip = text_after_eq.strip()
command_strip = text_strip.split(" ")[-1]
propositions = self.autocomplete(command_strip)
buffer = "--\n" + AUTOCOMPLETE_SEPARATOR.join(propositions)
buffer = buffer.strip()
if len(propositions) > 1:
self.remove_last_command()
self.write(buffer)
self.raw_input(self.prompt + command)
elif len(propositions) == 1:
self.remove_last_command()
# Replace text by last proposition
command = command.replace(command_strip, propositions[0])
self.write(self.prompt + command)
else:
return
[docs] @Slot()
def count_cursor_lines(self):
"""
Slot def keep tracking cursor position line number. Useful to compare position to know if it is an
editable line or not.
:return:
"""
cursor = self.textCursor()
cursor.movePosition(QTextCursor.StartOfLine)
lines = 1
while cursor.positionInBlock() > 0:
cursor.movePosition(QTextCursor.Up)
lines += 1
block = cursor.block().previous()
while block.isValid():
lines += block.lineCount()
block = block.previous()
self.cursor_line = lines
[docs] def exec_code(self, cmd):
self.thread = QtCore.QThread()
self.exec_thread = ExecThread()
self.exec_thread.cmd = cmd
self.exec_thread.def_to_run = self.def_to_run_code
self.exec_thread.moveToThread(self.thread)
self.thread.started.connect(self.exec_thread.run)
self.exec_thread.finished.connect(self.thread.quit)
self.thread.start()
[docs] def keyPressEvent(self, event):
"""
Override to manage key board event.
:param event: event key.
:return:
"""
# Is an editable line ? if not go to the last line.
if self.cursor_line != self.get_last_line() and not self.textCursor().hasSelection():
self.moveCursor(QTextCursor.End)
# Enter key pressed to run code.
if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
cmd = self.get_command()
self.press_enter.emit(cmd)
# add to the history.
if bool(cmd and cmd.strip()):
readline.add_history(cmd)
self.history_index = readline.get_current_history_length()
return
# Avoid delete prompt or text before.
elif event.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Backspace):
if self.get_cursor_position() == 0:
return
# History navigation.
elif event.key() == QtCore.Qt.Key_Down:
self.remove_last_command()
self.raw_input(self.prompt + self.get_previous_history())
return
elif event.key() == QtCore.Qt.Key_Up:
self.remove_last_command()
self.raw_input(self.prompt + self.get_next_history())
return
# Tab autocomplete
elif event.key() == QtCore.Qt.Key_Tab:
cmd = self.get_command()
if bool(cmd and cmd.strip()):
self.write_autocomplete(cmd)
#else:
# self.write_prompt(" ")
return
super(Terminal, self).keyPressEvent(event)
[docs] def closeEvent(self, event):
self.thread_q.stop()
self.thread_q.exit()