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 import sys
from calibre import prints as prints_ 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): def prints(*args, **kwargs):
kwargs['file'] = sys.__stdout__ kwargs['file'] = sys.__stdout__

View File

@ -5,9 +5,10 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __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 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.formatter import Formatter
from calibre.utils.pyconsole.repl import Interpreter, DummyFile from calibre.utils.pyconsole.repl import Interpreter, DummyFile
from calibre.utils.pyconsole import prints from calibre.utils.pyconsole import prints
from calibre.gui2 import error_dialog
class EditBlock(object): # {{{ class EditBlock(object): # {{{
@ -29,8 +31,27 @@ class EditBlock(object): # {{{
self.cursor.endEditBlock() 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): class Console(QTextEdit):
running = pyqtSignal()
running_done = pyqtSignal()
@property @property
def doc(self): def doc(self):
return self.document() return self.document()
@ -43,6 +64,23 @@ class Console(QTextEdit):
def root_frame(self): def root_frame(self):
return self.doc.rootFrame() 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, def __init__(self,
prompt='>>> ', prompt='>>> ',
@ -60,7 +98,17 @@ class Console(QTextEdit):
self.doc.setMaximumBlockCount(10000) self.doc.setMaximumBlockCount(10000)
self.lexer = PythonLexer(ensurenl=False) self.lexer = PythonLexer(ensurenl=False)
self.tb_lexer = PythonTracebackLexer() 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('''\ motd = textwrap.dedent('''\
# Python {0} # Python {0}
@ -76,6 +124,8 @@ class Console(QTextEdit):
self.interpreter = Interpreter(parent=self) self.interpreter = Interpreter(parent=self)
self.interpreter.show_error.connect(self.show_error) self.interpreter.show_error.connect(self.show_error)
sys.excepthook = self.unhandled_exception
# Prompt management {{{ # Prompt management {{{
@ -162,6 +212,8 @@ class Console(QTextEdit):
if row > -1 and restore_cursor: if row > -1 and restore_cursor:
self.cursor_pos = (row, col) self.cursor_pos = (row, col)
self.ensureCursorVisible()
# }}} # }}}
# Non-prompt Rendering {{{ # Non-prompt Rendering {{{
@ -185,16 +237,26 @@ class Console(QTextEdit):
self.formatter.render(self.tb_lexer.get_tokens(tb), self.cursor) self.formatter.render(self.tb_lexer.get_tokens(tb), self.cursor)
except: except:
prints(tb, end='') prints(tb, end='')
self.ensureCursorVisible()
QCoreApplication.processEvents()
def show_output(self, raw): def show_output(self, raw):
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: if self.prompt_frame is not None:
# At a prompt, so redirect output with Prepender(self):
return prints(raw, end='') do_show()
try: else:
self.buf.append(raw) do_show()
self.formatter.render_raw(raw, self.cursor) self.ensureCursorVisible()
except: QCoreApplication.processEvents()
prints(raw, end='')
# }}} # }}}
@ -203,16 +265,11 @@ class Console(QTextEdit):
def keyPressEvent(self, ev): def keyPressEvent(self, ev):
text = unicode(ev.text()) text = unicode(ev.text())
key = ev.key() key = ev.key()
if key in (Qt.Key_Enter, Qt.Key_Return): action = self.key_dispatcher.get(key, None)
self.enter_pressed() if callable(action):
elif key == Qt.Key_Home: action()
self.home_pressed() elif key in (Qt.Key_Escape,):
elif key == Qt.Key_End: QTextEdit.keyPressEvent(self, ev)
self.end_pressed()
elif key == Qt.Key_Left:
self.left_pressed()
elif key == Qt.Key_Right:
self.right_pressed()
elif text: elif text:
self.text_typed(text) self.text_typed(text)
else: else:
@ -230,6 +287,7 @@ class Console(QTextEdit):
c.movePosition(c.Up) c.movePosition(c.Up)
c.movePosition(c.EndOfLine) c.movePosition(c.EndOfLine)
self.setTextCursor(c) self.setTextCursor(c)
self.ensureCursorVisible()
def right_pressed(self): def right_pressed(self):
lineno, pos = self.cursor_pos lineno, pos = self.cursor_pos
@ -242,6 +300,7 @@ class Console(QTextEdit):
elif lineno < len(cp)-1: elif lineno < len(cp)-1:
c.movePosition(c.NextCharacter, n=1+self.prompt_len) c.movePosition(c.NextCharacter, n=1+self.prompt_len)
self.setTextCursor(c) self.setTextCursor(c)
self.ensureCursorVisible()
def home_pressed(self): def home_pressed(self):
if self.prompt_frame is not None: if self.prompt_frame is not None:
@ -249,12 +308,14 @@ class Console(QTextEdit):
c.movePosition(c.StartOfLine) c.movePosition(c.StartOfLine)
c.movePosition(c.NextCharacter, n=self.prompt_len) c.movePosition(c.NextCharacter, n=self.prompt_len)
self.setTextCursor(c) self.setTextCursor(c)
self.ensureCursorVisible()
def end_pressed(self): def end_pressed(self):
if self.prompt_frame is not None: if self.prompt_frame is not None:
c = self.cursor c = self.cursor
c.movePosition(c.EndOfLine) c.movePosition(c.EndOfLine)
self.setTextCursor(c) self.setTextCursor(c)
self.ensureCursorVisible()
def enter_pressed(self): def enter_pressed(self):
if self.prompt_frame is None: if self.prompt_frame is None:
@ -267,7 +328,13 @@ class Console(QTextEdit):
self.prompt_frame = None self.prompt_frame = None
oldbuf = self.buf oldbuf = self.buf
self.buf = [] self.buf = []
ret = self.interpreter.runsource('\n'.join(cp)) 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 if ret: # Incomplete command
self.buf = oldbuf self.buf = oldbuf
self.prompt_frame = old_pf self.prompt_frame = old_pf
@ -275,7 +342,13 @@ class Console(QTextEdit):
c.insertBlock() c.insertBlock()
self.setTextCursor(c) self.setTextCursor(c)
else: # Command completed else: # Command completed
old_pf.setFrameFormat(QTextFrameFormat()) try:
old_pf.setFrameFormat(QTextFrameFormat())
except RuntimeError:
# Happens if enough lines of output that the old
# frame was deleted
pass
self.render_current_prompt() self.render_current_prompt()
def text_typed(self, text): def text_typed(self, text):

View File

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

View File

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