Added a rudimentary console for easier interaction with Pesterchum's code. Still WIP.
This commit is contained in:
parent
0b36b45ddb
commit
dabd2b46c1
2 changed files with 353 additions and 1 deletions
352
console.py
Normal file
352
console.py
Normal file
|
@ -0,0 +1,352 @@
|
||||||
|
# vim: set autoindent ts=4 softtabstop=4 shiftwidth=4 textwidth=79 expandtab:
|
||||||
|
# -*- coding=UTF-8; tab-width: 4 -*-
|
||||||
|
|
||||||
|
from PyQt4 import QtGui, QtCore
|
||||||
|
import re, os, traceback, sys
|
||||||
|
import time, datetime
|
||||||
|
from os import remove
|
||||||
|
|
||||||
|
import dataobjs, generic, memos, parsetools, ostools
|
||||||
|
from version import _pcVersion
|
||||||
|
|
||||||
|
_datadir = ostools.getDataDir()
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logging.basicConfig(level=logging.WARNING)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleWindow(QtGui.QDialog):
|
||||||
|
# A simple console class, cobbled together from the corpse of another.
|
||||||
|
|
||||||
|
textArea = None
|
||||||
|
textInput = None
|
||||||
|
history = None
|
||||||
|
# I should probably put up constants for 'direction' if this is going to
|
||||||
|
# get this complicated. TODO!
|
||||||
|
incoming_prefix = "<<<"
|
||||||
|
outgoing_prefix = ">>>"
|
||||||
|
neutral_prefix = "!!!"
|
||||||
|
waiting_prefix = "..."
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
super(ConsoleWindow, self).__init__(parent)
|
||||||
|
self.prnt = parent
|
||||||
|
try:
|
||||||
|
self.mainwindow = parent.mainwindow
|
||||||
|
except:
|
||||||
|
self.mainwindow = parent
|
||||||
|
theme = self.mainwindow.theme
|
||||||
|
|
||||||
|
# Set up our style/window specifics
|
||||||
|
self.setStyleSheet(theme["main/defaultwindow/style"])
|
||||||
|
self.setWindowTitle("==> Console")
|
||||||
|
self.resize(350,300)
|
||||||
|
|
||||||
|
self.textArea = ConsoleText(theme, self)
|
||||||
|
self.textInput = ConsoleInput(theme, self)
|
||||||
|
self.textInput.setFocus()
|
||||||
|
|
||||||
|
self.connect(self.textInput, QtCore.SIGNAL('returnPressed()'),
|
||||||
|
self, QtCore.SLOT('sentMessage()'))
|
||||||
|
|
||||||
|
self.chumopen = True
|
||||||
|
self.chum = self.mainwindow.profile()
|
||||||
|
self.history = dataobjs.PesterHistory()
|
||||||
|
|
||||||
|
# For backing these up
|
||||||
|
self.stdout = self.stderr = None
|
||||||
|
|
||||||
|
layout_0 = QtGui.QVBoxLayout()
|
||||||
|
layout_0.addWidget(self.textArea)
|
||||||
|
layout_0.addWidget(self.textInput)
|
||||||
|
self.setLayout(layout_0)
|
||||||
|
|
||||||
|
def parent(self):
|
||||||
|
return self.prnt
|
||||||
|
|
||||||
|
def clearNewMessage(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot()
|
||||||
|
def sentMessage(self):
|
||||||
|
text = self.textInput.text()
|
||||||
|
# TODO: Make this deal with unicode text, it'll crash and burn as-is.
|
||||||
|
text = str(text)
|
||||||
|
text = text.rstrip()
|
||||||
|
|
||||||
|
self.history.add(text)
|
||||||
|
self.textInput.setText("")
|
||||||
|
|
||||||
|
self.execInConsole(text)
|
||||||
|
# Scroll down to the bottom so we can see the results.
|
||||||
|
sb = self.textArea.verticalScrollBar()
|
||||||
|
sb.setValue(sb.maximum())
|
||||||
|
|
||||||
|
def addTraceback(self, tb=None):
|
||||||
|
# We should do the formatting here, but eventually pass it to textArea
|
||||||
|
# to addMessage whatever output we produced.
|
||||||
|
# If we're called by addMessage - and we should be - then sys.stdout is
|
||||||
|
# still being redirected into the console.
|
||||||
|
# TODO: Just make an object for setting contexts (and thus optionally
|
||||||
|
# redirecting prints). Use 'with', of course.
|
||||||
|
# TODO: Make this exclude *our* processing from the traceback stack.
|
||||||
|
try:
|
||||||
|
self.addMessage(traceback.format_exc(), direction=0)
|
||||||
|
except Exception as err:
|
||||||
|
logging.error("Failed to display error message (???): %s" % err)
|
||||||
|
|
||||||
|
def addMessage(self, msg, direction):
|
||||||
|
# Redirect to where these things belong.
|
||||||
|
self.textArea.addMessage(msg, direction=direction)
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
# TODO: Set up ESC to close the console...or refer to hiding it as
|
||||||
|
# closing it. Not sure which is preferable.
|
||||||
|
parent = self.parent()
|
||||||
|
parent.console.is_open = False
|
||||||
|
parent.console.window = None
|
||||||
|
|
||||||
|
# Actual console stuff.
|
||||||
|
def execInConsole(self, scriptstr, env=None):
|
||||||
|
# Since that's what imports *us*, this should be okay
|
||||||
|
# Tab completion could be set up in ConsoleInput, and would be nice
|
||||||
|
import pesterchum as pchum
|
||||||
|
|
||||||
|
if env is None:
|
||||||
|
env = pchum._retrieveGlobals()
|
||||||
|
|
||||||
|
# Modify the environment the script will execute in.
|
||||||
|
# NOTE: This doesn't presently work, for some reason.
|
||||||
|
_CUSTOM_ENV = {
|
||||||
|
"CONSOLE": self,
|
||||||
|
"MAINWIN": self.mainwindow,
|
||||||
|
"PCONFIG": self.mainwindow.config
|
||||||
|
}
|
||||||
|
for k in _CUSTOM_ENV:
|
||||||
|
if k not in env:
|
||||||
|
# Don't overwrite anything!
|
||||||
|
env[k] = _CUSTOM_ENV[k]
|
||||||
|
else:
|
||||||
|
warn = "Console environment item {!r} already exists in main."
|
||||||
|
warn.format(k)
|
||||||
|
logging.warning(warn)
|
||||||
|
|
||||||
|
# Display the input we provided
|
||||||
|
self.addMessage(scriptstr, 1)
|
||||||
|
# Replace the old writer (for now)
|
||||||
|
sysout, sys.stdout = sys.stdout, self
|
||||||
|
try:
|
||||||
|
code = compile(scriptstr + '\n', "<string>", "single")
|
||||||
|
result = eval(code, env)
|
||||||
|
except:
|
||||||
|
# Something went wrong.
|
||||||
|
#~logging.exception("Exception: %s", err, exc_info=sys.exc_info())
|
||||||
|
self.addTraceback(sys.exc_info()[2])
|
||||||
|
else:
|
||||||
|
# No errors.
|
||||||
|
if result is not None:
|
||||||
|
#~self.addMessage(repr(result), -1)
|
||||||
|
print repr(result)
|
||||||
|
finally:
|
||||||
|
# Restore system output.
|
||||||
|
sys.stdout = sysout
|
||||||
|
|
||||||
|
# Try to clean us out of globals - this might be disabled
|
||||||
|
# later.
|
||||||
|
for k in _CUSTOM_ENV:
|
||||||
|
# Remove the key we added.
|
||||||
|
env.pop(k, None)
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
# Replaces sys.stdout briefly
|
||||||
|
# We only ever use this for receiving, so it's safe to assume the
|
||||||
|
# direction is always -1.
|
||||||
|
if not isinstance(data, list):
|
||||||
|
data = data.split('\n')
|
||||||
|
for line in data:
|
||||||
|
if len(line):
|
||||||
|
self.addMessage(line, -1)
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleText(QtGui.QTextEdit):
|
||||||
|
def __init__(self, theme, parent=None):
|
||||||
|
super(ConsoleText, self).__init__(parent)
|
||||||
|
if hasattr(self.parent(), 'mainwindow'):
|
||||||
|
self.mainwindow = self.parent().mainwindow
|
||||||
|
else:
|
||||||
|
self.mainwindow = self.parent()
|
||||||
|
|
||||||
|
self.hasTabs = False
|
||||||
|
self.initTheme(theme)
|
||||||
|
self.setReadOnly(True)
|
||||||
|
self.setMouseTracking(True)
|
||||||
|
self.textSelected = False
|
||||||
|
|
||||||
|
self.connect(self, QtCore.SIGNAL('copyAvailable(bool)'),
|
||||||
|
self, QtCore.SLOT('textReady(bool)'))
|
||||||
|
self.urls = {}
|
||||||
|
|
||||||
|
# Stripped out animation init - it's all cruft to us.
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot(bool)
|
||||||
|
def textReady(self, ready):
|
||||||
|
self.textSelected = ready
|
||||||
|
|
||||||
|
def initTheme(self, theme):
|
||||||
|
if theme.has_key("convo/scrollbar"):
|
||||||
|
self.setStyleSheet("QTextEdit { %s } QScrollBar:vertical { %s } QScrollBar::handle:vertical { %s } QScrollBar::add-line:vertical { %s } QScrollBar::sub-line:vertical { %s } QScrollBar:up-arrow:vertical { %s } QScrollBar:down-arrow:vertical { %s }" % (theme["convo/textarea/style"], theme["convo/scrollbar/style"], theme["convo/scrollbar/handle"], theme["convo/scrollbar/downarrow"], theme["convo/scrollbar/uparrow"], theme["convo/scrollbar/uarrowstyle"], theme["convo/scrollbar/darrowstyle"] ))
|
||||||
|
else:
|
||||||
|
self.setStyleSheet("QTextEdit { %s }" % (theme["convo/textarea/style"]))
|
||||||
|
|
||||||
|
def addMessage(self, msg, direction):
|
||||||
|
# Display a message we've received.
|
||||||
|
# Direction > 0 == out (sent by us); < 0 == in (sent by script).
|
||||||
|
if len(msg) == 0:
|
||||||
|
return
|
||||||
|
#~color = chum.colorcmd()
|
||||||
|
#~initials = chum.initials()
|
||||||
|
parent = self.parent()
|
||||||
|
mwindow = parent.mainwindow
|
||||||
|
|
||||||
|
systemColor = QtGui.QColor(mwindow.theme["convo/systemMsgColor"])
|
||||||
|
|
||||||
|
if mwindow.config.showTimeStamps():
|
||||||
|
if mwindow.config.time12Format():
|
||||||
|
timestamp = time.strftime("[%I:%M")
|
||||||
|
else:
|
||||||
|
timestamp = time.strftime("[%H:%M")
|
||||||
|
if mwindow.config.showSeconds():
|
||||||
|
timestamp += time.strftime(":%S] ")
|
||||||
|
else:
|
||||||
|
timestamp += "] "
|
||||||
|
else:
|
||||||
|
timestamp = ""
|
||||||
|
|
||||||
|
# Figure out what prefix to use.
|
||||||
|
if direction > 0:
|
||||||
|
# Outgoing.
|
||||||
|
prefix = parent.outgoing_prefix
|
||||||
|
elif direction < 0:
|
||||||
|
# Incoming.
|
||||||
|
prefix = parent.incoming_prefix
|
||||||
|
elif direction == 0:
|
||||||
|
# We could just 'else' here, but there might be some oddness later.
|
||||||
|
prefix = parent.neutral_prefix
|
||||||
|
|
||||||
|
# Later, this will have to escape things so we don't parse them,
|
||||||
|
# likely...hm.
|
||||||
|
#~result = "<span style=\"color:#000000\">{} {} {!r}</span>"
|
||||||
|
# The input we get is already repr'd...we pass it via print, and thus
|
||||||
|
# do that there.
|
||||||
|
result = "{}{} {}\n"
|
||||||
|
result = result.format(timestamp, prefix, msg)
|
||||||
|
#~self.append(result)
|
||||||
|
self.insertPlainText(result)
|
||||||
|
|
||||||
|
# Direction doesn't matter here - it's the console.
|
||||||
|
self.lastmsg = datetime.datetime.now()
|
||||||
|
# This needs to finish being rewritten....
|
||||||
|
|
||||||
|
def changeTheme(self, theme):
|
||||||
|
self.initTheme(theme)
|
||||||
|
sb = self.verticalScrollBar()
|
||||||
|
sb.setValue(sb.maximum())
|
||||||
|
|
||||||
|
def focusInEvent(self, event):
|
||||||
|
self.parent().clearNewMessage()
|
||||||
|
super(ConsoleText, self).focusInEvent(event)
|
||||||
|
|
||||||
|
def keyPressEvent(self, event):
|
||||||
|
# NOTE: This doesn't give focus to the input bar, which it probably
|
||||||
|
# should.
|
||||||
|
# karxi: Test for tab changing?
|
||||||
|
if hasattr(self.parent(), 'textInput'):
|
||||||
|
if event.key() not in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
|
||||||
|
QtCore.Qt.Key_Up, QtCore.Qt.Key_Down):
|
||||||
|
self.parent().textInput.keyPressEvent(event)
|
||||||
|
|
||||||
|
super(ConsoleText, self).keyPressEvent(event)
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
if event.button() == QtCore.Qt.LeftButton:
|
||||||
|
url = self.anchorAt(event.pos())
|
||||||
|
if url != "":
|
||||||
|
# Skip memo/handle recognition
|
||||||
|
# NOTE: Ctrl+Click copies the URL. Maybe it should select it?
|
||||||
|
if event.modifiers() == QtCore.Qt.ControlModifier:
|
||||||
|
QtGui.QApplication.clipboard().setText(url)
|
||||||
|
else:
|
||||||
|
# This'll probably be removed. May change the lexer out.
|
||||||
|
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url, QtCore.QUrl.TolerantMode))
|
||||||
|
|
||||||
|
super(ConsoleText, self).mousePressEvent(event)
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event):
|
||||||
|
# Change our cursor when we roll over links (anchors).
|
||||||
|
super(ConsoleText, self).mouseMoveEvent(event)
|
||||||
|
if self.anchorAt(event.pos()):
|
||||||
|
if self.viewport().cursor().shape != QtCore.Qt.PointingHandCursor:
|
||||||
|
self.viewport().setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
|
||||||
|
else:
|
||||||
|
self.viewport().setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
|
||||||
|
|
||||||
|
def contextMenuEvent(self, event):
|
||||||
|
# This is almost certainly no longer necessary.
|
||||||
|
textMenu = self.createStandardContextMenu()
|
||||||
|
#~if self.textSelected:
|
||||||
|
#~ self.submitLogAction = QtGui.QAction("Submit to Pesterchum QDB", self)
|
||||||
|
#~ self.connect(self.submitLogAction, QtCore.SIGNAL('triggered()'),
|
||||||
|
#~ self, QtCore.SLOT('submitLog()'))
|
||||||
|
#~ textMenu.addAction(self.submitLogAction)
|
||||||
|
textMenu.exec_(event.globalPos())
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleInput(QtGui.QLineEdit):
|
||||||
|
"""The actual text entry box on a ConsoleWindow."""
|
||||||
|
# I honestly feel like this could just be made a private class of
|
||||||
|
# ConsoleWindow, but...best not to overcomplicate things.
|
||||||
|
stylesheet_path = "convo/input/style"
|
||||||
|
|
||||||
|
def __init__(self, theme, parent=None):
|
||||||
|
super(ConsoleInput, self).__init__(parent)
|
||||||
|
|
||||||
|
self.changeTheme(theme)
|
||||||
|
|
||||||
|
def changeTheme(self, theme):
|
||||||
|
self.setStyleSheet(theme[self.stylesheet_path])
|
||||||
|
|
||||||
|
def focusInEvent(self, event):
|
||||||
|
# We gained focus. Notify the parent window that this happened.
|
||||||
|
self.parent().clearNewMessage()
|
||||||
|
self.parent().textArea.textCursor().clearSelection()
|
||||||
|
|
||||||
|
super(ConsoleInput, self).focusInEvent(event)
|
||||||
|
|
||||||
|
def keyPressEvent(self, event):
|
||||||
|
evtkey = event.key()
|
||||||
|
parent = self.parent()
|
||||||
|
|
||||||
|
# If a key is pressed here, we're not idle....
|
||||||
|
# NOTE: Do we really want everyone knowing we're around if we're
|
||||||
|
# messing around in the console? Hm.
|
||||||
|
parent.mainwindow.idler.time = 0
|
||||||
|
|
||||||
|
if evtkey == QtCore.Qt.Key_Up:
|
||||||
|
text = unicode(self.text())
|
||||||
|
next = parent.history.next(text)
|
||||||
|
if next is not None:
|
||||||
|
self.setText(next)
|
||||||
|
elif evtkey == QtCore.Qt.Key_Down:
|
||||||
|
prev = parent.history.prev()
|
||||||
|
if prev is not None:
|
||||||
|
self.setText(prev)
|
||||||
|
elif evtkey in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown):
|
||||||
|
parent.textArea.keyPressEvent(event)
|
||||||
|
else:
|
||||||
|
super(ConsoleInput, self).keyPressEvent(event)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
2
convo.py
2
convo.py
|
@ -542,7 +542,7 @@ class PesterInput(QtGui.QLineEdit):
|
||||||
|
|
||||||
class PesterConvo(QtGui.QFrame):
|
class PesterConvo(QtGui.QFrame):
|
||||||
def __init__(self, chum, initiated, mainwindow, parent=None):
|
def __init__(self, chum, initiated, mainwindow, parent=None):
|
||||||
QtGui.QFrame.__init__(self, parent)
|
super(PesterConvo, self).__init__(parent)
|
||||||
self.setAttribute(QtCore.Qt.WA_QuitOnClose, False)
|
self.setAttribute(QtCore.Qt.WA_QuitOnClose, False)
|
||||||
self.setObjectName(chum.handle)
|
self.setObjectName(chum.handle)
|
||||||
self.setFocusPolicy(QtCore.Qt.ClickFocus)
|
self.setFocusPolicy(QtCore.Qt.ClickFocus)
|
||||||
|
|
Loading…
Reference in a new issue