diff --git a/imgsrc/console.svg b/imgsrc/console.svg
new file mode 100644
index 0000000000..0d502bb1da
--- /dev/null
+++ b/imgsrc/console.svg
@@ -0,0 +1,4339 @@
+
+
+
diff --git a/resources/images/console.png b/resources/images/console.png
new file mode 100644
index 0000000000..168f0ccb2a
Binary files /dev/null and b/resources/images/console.png differ
diff --git a/src/calibre/utils/pyconsole/__init__.py b/src/calibre/utils/pyconsole/__init__.py
index a7cb4eed01..0dfa9398e1 100644
--- a/src/calibre/utils/pyconsole/__init__.py
+++ b/src/calibre/utils/pyconsole/__init__.py
@@ -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__
diff --git a/src/calibre/utils/pyconsole/console.py b/src/calibre/utils/pyconsole/console.py
index 19a24dfdd7..251e8424a0 100644
--- a/src/calibre/utils/pyconsole/console.py
+++ b/src/calibre/utils/pyconsole/console.py
@@ -5,9 +5,10 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
__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 = '%s:'%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,16 +237,26 @@ 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):
+ 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:
- # At a prompt, so redirect output
- return prints(raw, end='')
- try:
- self.buf.append(raw)
- self.formatter.render_raw(raw, self.cursor)
- except:
- prints(raw, end='')
+ with Prepender(self):
+ do_show()
+ else:
+ do_show()
+ self.ensureCursorVisible()
+ QCoreApplication.processEvents()
# }}}
@@ -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 = []
- 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
self.buf = oldbuf
self.prompt_frame = old_pf
@@ -275,7 +342,13 @@ class Console(QTextEdit):
c.insertBlock()
self.setTextCursor(c)
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()
def text_typed(self, text):
diff --git a/src/calibre/utils/pyconsole/formatter.py b/src/calibre/utils/pyconsole/formatter.py
index 7f99983ef6..9409007ec6 100644
--- a/src/calibre/utils/pyconsole/formatter.py
+++ b/src/calibre/utils/pyconsole/formatter.py
@@ -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)
diff --git a/src/calibre/utils/pyconsole/main.py b/src/calibre/utils/pyconsole/main.py
index af99ec66bb..f098ce2ee2 100644
--- a/src/calibre/utils/pyconsole/main.py
+++ b/src/calibre/utils/pyconsole/main.py
@@ -6,19 +6,31 @@ __copyright__ = '2010, Kovid Goyal '
__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_()