mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
65cebfd378
commit
fe6d962bee
4339
imgsrc/console.svg
Normal file
4339
imgsrc/console.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 113 KiB |
BIN
resources/images/console.png
Normal file
BIN
resources/images/console.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
@ -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__
|
||||||
|
@ -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,17 +237,27 @@ 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):
|
||||||
if self.prompt_frame is not None:
|
def do_show():
|
||||||
# At a prompt, so redirect output
|
|
||||||
return prints(raw, end='')
|
|
||||||
try:
|
try:
|
||||||
self.buf.append(raw)
|
self.buf.append(raw)
|
||||||
self.formatter.render_raw(raw, self.cursor)
|
self.formatter.render_raw(raw, self.cursor)
|
||||||
except:
|
except:
|
||||||
|
import traceback
|
||||||
|
prints(traceback.format_exc())
|
||||||
prints(raw, end='')
|
prints(raw, end='')
|
||||||
|
|
||||||
|
if self.prompt_frame is not None:
|
||||||
|
with Prepender(self):
|
||||||
|
do_show()
|
||||||
|
else:
|
||||||
|
do_show()
|
||||||
|
self.ensureCursorVisible()
|
||||||
|
QCoreApplication.processEvents()
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Keyboard handling {{{
|
# Keyboard handling {{{
|
||||||
@ -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 = []
|
||||||
|
self.running.emit()
|
||||||
|
try:
|
||||||
ret = self.interpreter.runsource('\n'.join(cp))
|
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
|
||||||
|
try:
|
||||||
old_pf.setFrameFormat(QTextFrameFormat())
|
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):
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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_()
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user