Use dialog instead of main window. Set console stylesheet based on pygments style. Don't block if there is a lot of output

This commit is contained in:
Kovid Goyal 2010-09-20 19:33:17 -06:00
parent 65cebfd378
commit fe6d962bee
6 changed files with 4482 additions and 41 deletions

4339
imgsrc/console.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -8,6 +8,15 @@ __docformat__ = 'restructuredtext en'
import sys
from calibre import prints as prints_
from calibre.utils.config import Config, StringConfig
def console_config(defaults=None):
desc=_('Settings to control the calibre content server')
c = Config('console', desc) if defaults is None else StringConfig(defaults, desc)
c.add_opt('--theme', default='default', help='The color theme')
def prints(*args, **kwargs):
kwargs['file'] = sys.__stdout__

View File

@ -5,9 +5,10 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys, textwrap
import sys, textwrap, traceback, StringIO
from PyQt4.Qt import QTextEdit, Qt, QTextFrameFormat
from PyQt4.Qt import QTextEdit, Qt, QTextFrameFormat, pyqtSignal, \
QCoreApplication
from pygments.lexers import PythonLexer, PythonTracebackLexer
@ -15,6 +16,7 @@ from calibre.constants import __appname__, __version__
from calibre.utils.pyconsole.formatter import Formatter
from calibre.utils.pyconsole.repl import Interpreter, DummyFile
from calibre.utils.pyconsole import prints
from calibre.gui2 import error_dialog
class EditBlock(object): # {{{
@ -29,8 +31,27 @@ class EditBlock(object): # {{{
self.cursor.endEditBlock()
# }}}
class Prepender(object): # {{{
'Helper class to insert output before the current prompt'
def __init__(self, console):
self.console = console
def __enter__(self):
c = self.console
self.opos = c.cursor_pos
cur = c.prompt_frame.firstCursorPosition()
cur.movePosition(cur.PreviousCharacter)
c.setTextCursor(cur)
def __exit__(self, *args):
self.console.cursor_pos = self.opos
# }}}
class Console(QTextEdit):
running = pyqtSignal()
running_done = pyqtSignal()
@property
def doc(self):
return self.document()
@ -43,6 +64,23 @@ class Console(QTextEdit):
def root_frame(self):
return self.doc.rootFrame()
def unhandled_exception(self, type, value, tb):
if type == KeyboardInterrupt:
return
try:
sio = StringIO.StringIO()
traceback.print_exception(type, value, tb, file=sio)
fe = sio.getvalue()
prints(fe)
try:
val = unicode(value)
except:
val = repr(value)
msg = '<b>%s</b>:'%type.__name__ + val
error_dialog(self, _('ERROR: Unhandled exception'), msg,
det_msg=fe, show=True)
except BaseException:
pass
def __init__(self,
prompt='>>> ',
@ -60,7 +98,17 @@ class Console(QTextEdit):
self.doc.setMaximumBlockCount(10000)
self.lexer = PythonLexer(ensurenl=False)
self.tb_lexer = PythonTracebackLexer()
self.formatter = Formatter(prompt, continuation)
self.formatter = Formatter(prompt, continuation, style='default')
self.setStyleSheet(self.formatter.stylesheet)
self.key_dispatcher = { # {{{
Qt.Key_Enter : self.enter_pressed,
Qt.Key_Return : self.enter_pressed,
Qt.Key_Home : self.home_pressed,
Qt.Key_End : self.end_pressed,
Qt.Key_Left : self.left_pressed,
Qt.Key_Right : self.right_pressed,
} # }}}
motd = textwrap.dedent('''\
# Python {0}
@ -76,6 +124,8 @@ class Console(QTextEdit):
self.interpreter = Interpreter(parent=self)
self.interpreter.show_error.connect(self.show_error)
sys.excepthook = self.unhandled_exception
# Prompt management {{{
@ -162,6 +212,8 @@ class Console(QTextEdit):
if row > -1 and restore_cursor:
self.cursor_pos = (row, col)
self.ensureCursorVisible()
# }}}
# Non-prompt Rendering {{{
@ -185,17 +237,27 @@ class Console(QTextEdit):
self.formatter.render(self.tb_lexer.get_tokens(tb), self.cursor)
except:
prints(tb, end='')
self.ensureCursorVisible()
QCoreApplication.processEvents()
def show_output(self, raw):
if self.prompt_frame is not None:
# At a prompt, so redirect output
return prints(raw, end='')
def do_show():
try:
self.buf.append(raw)
self.formatter.render_raw(raw, self.cursor)
except:
import traceback
prints(traceback.format_exc())
prints(raw, end='')
if self.prompt_frame is not None:
with Prepender(self):
do_show()
else:
do_show()
self.ensureCursorVisible()
QCoreApplication.processEvents()
# }}}
# Keyboard handling {{{
@ -203,16 +265,11 @@ class Console(QTextEdit):
def keyPressEvent(self, ev):
text = unicode(ev.text())
key = ev.key()
if key in (Qt.Key_Enter, Qt.Key_Return):
self.enter_pressed()
elif key == Qt.Key_Home:
self.home_pressed()
elif key == Qt.Key_End:
self.end_pressed()
elif key == Qt.Key_Left:
self.left_pressed()
elif key == Qt.Key_Right:
self.right_pressed()
action = self.key_dispatcher.get(key, None)
if callable(action):
action()
elif key in (Qt.Key_Escape,):
QTextEdit.keyPressEvent(self, ev)
elif text:
self.text_typed(text)
else:
@ -230,6 +287,7 @@ class Console(QTextEdit):
c.movePosition(c.Up)
c.movePosition(c.EndOfLine)
self.setTextCursor(c)
self.ensureCursorVisible()
def right_pressed(self):
lineno, pos = self.cursor_pos
@ -242,6 +300,7 @@ class Console(QTextEdit):
elif lineno < len(cp)-1:
c.movePosition(c.NextCharacter, n=1+self.prompt_len)
self.setTextCursor(c)
self.ensureCursorVisible()
def home_pressed(self):
if self.prompt_frame is not None:
@ -249,12 +308,14 @@ class Console(QTextEdit):
c.movePosition(c.StartOfLine)
c.movePosition(c.NextCharacter, n=self.prompt_len)
self.setTextCursor(c)
self.ensureCursorVisible()
def end_pressed(self):
if self.prompt_frame is not None:
c = self.cursor
c.movePosition(c.EndOfLine)
self.setTextCursor(c)
self.ensureCursorVisible()
def enter_pressed(self):
if self.prompt_frame is None:
@ -267,7 +328,13 @@ class Console(QTextEdit):
self.prompt_frame = None
oldbuf = self.buf
self.buf = []
self.running.emit()
try:
ret = self.interpreter.runsource('\n'.join(cp))
except SystemExit:
ret = False
self.show_output('Raising SystemExit not allowed\n')
self.running_done.emit()
if ret: # Incomplete command
self.buf = oldbuf
self.prompt_frame = old_pf
@ -275,7 +342,13 @@ class Console(QTextEdit):
c.insertBlock()
self.setTextCursor(c)
else: # Command completed
try:
old_pf.setFrameFormat(QTextFrameFormat())
except RuntimeError:
# Happens if enough lines of output that the old
# frame was deleted
pass
self.render_current_prompt()
def text_typed(self, text):

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QTextCharFormat, QFont, QBrush, QColor
from pygments.formatter import Formatter as PF
from pygments.token import Token
from pygments.token import Token, Generic
class Formatter(object):
@ -22,11 +22,16 @@ class Formatter(object):
pf = PF(**options)
self.styles = {}
self.normal = self.base_fmt()
self.background_color = pf.style.background_color
self.color = 'black'
for ttype, ndef in pf.style:
fmt = self.base_fmt()
if ndef['color']:
fmt.setForeground(QBrush(QColor('#%s'%ndef['color'])))
fmt.setUnderlineColor(QColor('#%s'%ndef['color']))
if ttype == Generic.Output:
self.color = '#%s'%ndef['color']
if ndef['bold']:
fmt.setFontWeight(QFont.Bold)
if ndef['italic']:
@ -40,6 +45,11 @@ class Formatter(object):
self.styles[ttype] = fmt
self.stylesheet = '''
QTextEdit { color: %s; background-color: %s }
'''%(self.color, self.background_color)
def base_fmt(self):
fmt = QTextCharFormat()
fmt.setFontFamily('monospace')
@ -74,7 +84,7 @@ class Formatter(object):
def render_prompt(self, is_continuation, cursor):
pr = self.continuation if is_continuation else self.prompt
fmt = self.styles[Token.Generic.Subheading]
fmt = self.styles[Generic.Prompt]
cursor.insertText(pr, fmt)

View File

@ -6,19 +6,31 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
__version__ = '0.1.0'
from PyQt4.Qt import QMainWindow, QToolBar, QStatusBar, QLabel, QFont, Qt, \
QApplication
from functools import partial
from PyQt4.Qt import QDialog, QToolBar, QStatusBar, QLabel, QFont, Qt, \
QApplication, QIcon, QVBoxLayout
from calibre.constants import __appname__, __version__
from calibre.utils.pyconsole.console import Console
class MainWindow(QMainWindow):
class MainWindow(QDialog):
def __init__(self, default_status_msg):
def __init__(self,
default_status_msg=_('Welcome to') + ' ' + __appname__+' console',
parent=None):
QMainWindow.__init__(self)
QDialog.__init__(self, parent)
self.l = QVBoxLayout()
self.setLayout(self.l)
self.resize(600, 700)
self.resize(800, 600)
# Setup tool bar {{{
self.tool_bar = QToolBar(self)
self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextOnly)
self.l.addWidget(self.tool_bar)
# }}}
# Setup status bar {{{
self.status_bar = QStatusBar(self)
@ -28,25 +40,23 @@ class MainWindow(QMainWindow):
self.status_bar._font.setBold(True)
self.status_bar.defmsg.setFont(self.status_bar._font)
self.status_bar.addWidget(self.status_bar.defmsg)
self.setStatusBar(self.status_bar)
# }}}
# Setup tool bar {{{
self.tool_bar = QToolBar(self)
self.addToolBar(Qt.BottomToolBarArea, self.tool_bar)
self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextOnly)
# }}}
self.editor = Console(parent=self)
self.setCentralWidget(self.editor)
self.console = Console(parent=self)
self.console.running.connect(partial(self.status_bar.showMessage,
_('Code is running')))
self.console.running_done.connect(self.status_bar.clearMessage)
self.l.addWidget(self.console)
self.l.addWidget(self.status_bar)
self.setWindowTitle(__appname__ + ' console')
self.setWindowIcon(QIcon(I('console.png')))
def main():
QApplication.setApplicationName(__appname__+' console')
QApplication.setOrganizationName('Kovid Goyal')
app = QApplication([])
m = MainWindow(_('Welcome to') + ' ' + __appname__+' console')
m = MainWindow()
m.show()
app.exec_()