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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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_()