diff --git a/src/calibre/debug.py b/src/calibre/debug.py index c84ce3dfcc..8a2097ddd1 100644 --- a/src/calibre/debug.py +++ b/src/calibre/debug.py @@ -37,6 +37,8 @@ Run an embedded python interpreter. parser.add_option('--reinitialize-db', default=None, help='Re-initialize the sqlite calibre database at the ' 'specified path. Useful to recover from db corruption.') + parser.add_option('-p', '--py-console', help='Run python console', + default=False, action='store_true') return parser @@ -148,6 +150,9 @@ def main(args=sys.argv): if len(args) > 1: vargs.append(args[-1]) main(vargs) + elif opts.py_console: + from calibre.utils.pyconsole.main import main + main() elif opts.command: sys.argv = args[:1] exec opts.command diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 83cf6278e5..b01869deaa 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -11,11 +11,11 @@ from PyQt4 import QtGui from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.tag_editor import TagEditor -from calibre.ebooks.metadata import string_to_authors, \ - authors_to_string, MetaInformation +from calibre.ebooks.metadata import string_to_authors, authors_to_string from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2.dialogs.progress import BlockingBusy from calibre.gui2 import error_dialog, Dispatcher +from calibre.utils.config import dynamic class Worker(Thread): @@ -122,12 +122,20 @@ def format_composite(x, mi): class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): - s_r_functions = { - '' : lambda x: x, - _('Lower Case') : lambda x: x.lower(), - _('Upper Case') : lambda x: x.upper(), - _('Title Case') : lambda x: x.title(), - } + s_r_functions = { '' : lambda x: x, + _('Lower Case') : lambda x: x.lower(), + _('Upper Case') : lambda x: x.upper(), + _('Title Case') : lambda x: x.title(), + } + + s_r_match_modes = [ _('Character match'), + _('Regular Expression'), + ] + + s_r_replace_modes = [ _('Replace field'), + _('Prepend to field'), + _('Append to field'), + ] def __init__(self, window, rows, db): QDialog.__init__(self, window) @@ -179,79 +187,123 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): fields.sort() self.search_field.addItems(fields) self.search_field.setMaxVisibleItems(min(len(fields), 20)) + self.destination_field.addItems(fields) + self.destination_field.setMaxVisibleItems(min(len(fields), 20)) offset = 10 self.s_r_number_of_books = min(7, len(self.ids)) for i in range(1,self.s_r_number_of_books+1): w = QtGui.QLabel(self.tabWidgetPage3) w.setText(_('Book %d:')%i) - self.gridLayout1.addWidget(w, i+offset, 0, 1, 1) + self.testgrid.addWidget(w, i+offset, 0, 1, 1) w = QtGui.QLineEdit(self.tabWidgetPage3) w.setReadOnly(True) name = 'book_%d_text'%i setattr(self, name, w) self.book_1_text.setObjectName(name) - self.gridLayout1.addWidget(w, i+offset, 1, 1, 1) + self.testgrid.addWidget(w, i+offset, 1, 1, 1) w = QtGui.QLineEdit(self.tabWidgetPage3) w.setReadOnly(True) name = 'book_%d_result'%i setattr(self, name, w) self.book_1_text.setObjectName(name) - self.gridLayout1.addWidget(w, i+offset, 2, 1, 1) + self.testgrid.addWidget(w, i+offset, 2, 1, 1) - self.s_r_heading.setText('

'+ - _('Search and replace in text fields using ' - 'regular expressions. The search text is an ' - 'arbitrary python-compatible regular expression. ' - 'The replacement text can contain backreferences ' - 'to parenthesized expressions in the pattern. ' - 'The search is not anchored, and can match and ' - 'replace multiple times on the same string. See ' - ' ' - 'this reference ' - 'for more information, and in particular the \'sub\' ' - 'function.') + '

' + _( - 'Note: you can destroy your library ' - 'using this feature. Changes are permanent. There ' - 'is no undo function. You are strongly encouraged ' - 'to back up your library before proceeding.')) + self.s_r_heading.setText('

'+ _( + 'You can destroy your library using this feature. ' + 'Changes are permanent. There is no undo function. ' + ' This feature is experimental, and there may be bugs. ' + 'You are strongly encouraged to back up your library ' + 'before proceeding.' + ) + '

' + _( + 'Search and replace in text fields using character matching ' + 'or regular expressions. In character mode, search text ' + 'found in the specified field is replaced with replace ' + 'text. In regular expression mode, the search text is an ' + 'arbitrary python-compatible regular expression. The ' + 'replacement text can contain backreferences to parenthesized ' + 'expressions in the pattern. The search is not anchored, ' + 'and can match and replace multiple times on the same string. ' + 'See ' + 'this reference for more information, and in particular ' + 'the \'sub\' function.' + )) + self.search_mode.addItems(self.s_r_match_modes) + self.search_mode.setCurrentIndex(dynamic.get('s_r_search_mode', 0)) + self.replace_mode.addItems(self.s_r_replace_modes) + self.replace_mode.setCurrentIndex(0) + + self.s_r_search_mode = 0 self.s_r_error = None self.s_r_obj = None self.replace_func.addItems(sorted(self.s_r_functions.keys())) - self.search_field.currentIndexChanged[str].connect(self.s_r_field_changed) + self.search_mode.currentIndexChanged[int].connect(self.s_r_search_mode_changed) + self.search_field.currentIndexChanged[str].connect(self.s_r_search_field_changed) + self.destination_field.currentIndexChanged[str].connect(self.s_r_destination_field_changed) + + self.replace_mode.currentIndexChanged[int].connect(self.s_r_paint_results) self.replace_func.currentIndexChanged[str].connect(self.s_r_paint_results) self.search_for.editTextChanged[str].connect(self.s_r_paint_results) self.replace_with.editTextChanged[str].connect(self.s_r_paint_results) self.test_text.editTextChanged[str].connect(self.s_r_paint_results) + self.comma_separated.stateChanged.connect(self.s_r_paint_results) + self.case_sensitive.stateChanged.connect(self.s_r_paint_results) self.central_widget.setCurrentIndex(0) self.search_for.completer().setCaseSensitivity(Qt.CaseSensitive) self.replace_with.completer().setCaseSensitivity(Qt.CaseSensitive) + self.s_r_search_mode_changed(self.search_mode.currentIndex()) - def s_r_field_changed(self, txt): + def s_r_get_field(self, mi, field): + if field: + fm = self.db.metadata_for_field(field) + val = mi.get(field, None) + if val is None: + val = [] + elif not fm['is_multiple']: + val = [val] + elif field == 'authors': + val = [v.replace(',', '|') for v in val] + else: + val = [] + return val + + def s_r_search_field_changed(self, txt): txt = unicode(txt) for i in range(0, self.s_r_number_of_books): - if txt: - fm = self.db.field_metadata[txt] - id = self.ids[i] - val = self.db.get_property(id, index_is_id=True, - loc=fm['rec_index']) - if val is None: - val = '' - if fm['is_multiple']: - val = [t.strip() for t in val.split(fm['is_multiple']) if t.strip()] - if val: - val.sort(cmp=lambda x,y: cmp(x.lower(), y.lower())) - val = val[0] - if txt == 'authors': - val = val.replace('|', ',') - else: - val = '' - else: - val = '' w = getattr(self, 'book_%d_text'%(i+1)) - w.setText(val) + mi = self.db.get_metadata(self.ids[i], index_is_id=True) + src = unicode(self.search_field.currentText()) + t = self.s_r_get_field(mi, src) + w.setText(''.join(t[0:1])) + self.s_r_paint_results(None) + + def s_r_destination_field_changed(self, txt): + txt = unicode(txt) + self.comma_separated.setEnabled(True) + if txt: + fm = self.db.metadata_for_field(txt) + if fm['is_multiple']: + self.comma_separated.setEnabled(False) + self.comma_separated.setChecked(True) + self.s_r_paint_results(None) + + def s_r_search_mode_changed(self, val): + if val == 0: + self.destination_field.setCurrentIndex(0) + self.destination_field.setVisible(False) + self.destination_field_label.setVisible(False) + self.replace_mode.setCurrentIndex(0) + self.replace_mode.setVisible(False) + self.replace_mode_label.setVisible(False) + self.comma_separated.setVisible(False) + else: + self.destination_field.setVisible(True) + self.destination_field_label.setVisible(True) + self.replace_mode.setVisible(True) + self.replace_mode_label.setVisible(True) + self.comma_separated.setVisible(True) self.s_r_paint_results(None) def s_r_set_colors(self): @@ -265,32 +317,73 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): for i in range(0,self.s_r_number_of_books): getattr(self, 'book_%d_result'%(i+1)).setText('') - field_match_re = re.compile(r'(^|[^\\])(\\g<)([^>]+)(>)') - def s_r_func(self, match): rfunc = self.s_r_functions[unicode(self.replace_func.currentText())] rtext = unicode(self.replace_with.text()) - mi_data = self.mi.all_non_none_fields() - - def fm_func(m): - try: - if m.group(3) not in self.mi.all_field_keys(): return m.group(0) - else: return '%s{%s}'%(m.group(1), m.group(3)) - except: - import traceback - traceback.print_exc() - return m.group(0) - - rtext = re.sub(self.field_match_re, fm_func, rtext) rtext = match.expand(rtext) - rtext = format_composite(rtext, mi_data) return rfunc(rtext) + def s_r_do_regexp(self, mi): + src_field = unicode(self.search_field.currentText()) + src = self.s_r_get_field(mi, src_field) + result = [] + rfunc = self.s_r_functions[unicode(self.replace_func.currentText())] + for s in src: + t = self.s_r_obj.sub(self.s_r_func, s) + if self.search_mode.currentIndex() == 0: + t = rfunc(t) + result.append(t) + return result + + def s_r_do_destination(self, mi, val): + src = unicode(self.search_field.currentText()) + if src == '': + return '' + dest = unicode(self.destination_field.currentText()) + if dest == '': + dest = src + dest_mode = self.replace_mode.currentIndex() + + if dest_mode != 0: + dest_val = mi.get(dest, '') + if dest_val is None: + dest_val = [] + elif isinstance(dest_val, list): + if dest == 'authors': + dest_val = [v.replace(',', '|') for v in dest_val] + else: + dest_val = [dest_val] + else: + dest_val = [] + + if len(val) > 0: + if src == 'authors': + val = [v.replace(',', '|') for v in val] + if dest_mode == 1: + val.extend(dest_val) + elif dest_mode == 2: + val[0:0] = dest_val + return val + + def s_r_replace_mode_separator(self): + if self.comma_separated.isChecked(): + return ',' + return '' + def s_r_paint_results(self, txt): self.s_r_error = None self.s_r_set_colors() + + if self.case_sensitive.isChecked(): + flags = 0 + else: + flags = re.I + try: - self.s_r_obj = re.compile(unicode(self.search_for.text())) + if self.search_mode.currentIndex() == 0: + self.s_r_obj = re.compile(re.escape(unicode(self.search_for.text())), flags) + else: + self.s_r_obj = re.compile(unicode(self.search_for.text()), flags) except Exception as e: self.s_r_obj = None self.s_r_error = e @@ -298,7 +391,6 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): return try: - self.mi = MetaInformation(None, None) self.test_result.setText(self.s_r_obj.sub(self.s_r_func, unicode(self.test_text.text()))) except Exception as e: @@ -307,62 +399,56 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): return for i in range(0,self.s_r_number_of_books): - id = self.ids[i] - self.mi = self.db.get_metadata(id, index_is_id=True) - wt = getattr(self, 'book_%d_text'%(i+1)) + mi = self.db.get_metadata(self.ids[i], index_is_id=True) wr = getattr(self, 'book_%d_result'%(i+1)) try: - wr.setText(self.s_r_obj.sub(self.s_r_func, unicode(wt.text()))) + result = self.s_r_do_regexp(mi) + t = self.s_r_do_destination(mi, result[0:1]) + t = self.s_r_replace_mode_separator().join(t) + wr.setText(t) except Exception as e: + import traceback + traceback.print_exc() self.s_r_error = e self.s_r_set_colors() break def do_search_replace(self): - field = unicode(self.search_field.currentText()) - if not field or not self.s_r_obj: + source = unicode(self.search_field.currentText()) + if not source or not self.s_r_obj: return - - fm = self.db.field_metadata[field] - - def apply_pattern(val): - try: - return self.s_r_obj.sub(self.s_r_func, val) - except: - return val + dest = unicode(self.destination_field.currentText()) + if not dest: + dest = source + dfm = self.db.field_metadata[dest] for id in self.ids: - val = self.db.get_property(id, index_is_id=True, - loc=fm['rec_index']) + mi = self.db.get_metadata(id, index_is_id=True,) + val = mi.get(source) if val is None: continue - if fm['is_multiple']: - res = [] - for val in [t.strip() for t in val.split(fm['is_multiple'])]: - v = apply_pattern(val).strip() - if v: - res.append(v) - val = res - if fm['is_custom']: + val = self.s_r_do_regexp(mi) + val = self.s_r_do_destination(mi, val) + if dfm['is_multiple']: + if dfm['is_custom']: # The standard tags and authors values want to be lists. # All custom columns are to be strings - val = fm['is_multiple'].join(val) - elif field == 'authors': - val = [v.replace('|', ',') for v in val] + val = dfm['is_multiple'].join(val) else: - val = apply_pattern(val) + val = self.s_r_replace_mode_separator().join(val) - if fm['is_custom']: - extra = self.db.get_custom_extra(id, label=fm['label'], index_is_id=True) - self.db.set_custom(id, val, label=fm['label'], extra=extra, + if dfm['is_custom']: + extra = self.db.get_custom_extra(id, label=dfm['label'], index_is_id=True) + self.db.set_custom(id, val, label=dfm['label'], extra=extra, commit=False) else: - if field == 'comments': + if dest == 'comments': setter = self.db.set_comment else: - setter = getattr(self.db, 'set_'+field) + setter = getattr(self.db, 'set_'+dest) setter(id, val, notify=False, commit=False) self.db.commit() + dynamic['s_r_search_mode'] = self.search_mode.currentIndex() def create_custom_column_editors(self): w = self.central_widget.widget(1) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index aca7b0cb75..e433aaf327 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -319,7 +319,7 @@ Future conversion of these books will use the default settings. &Search and replace (experimental) - + QLayout::SetMinimumSize @@ -351,6 +351,39 @@ Future conversion of these books will use the default settings. + + + + + + + + Search mode: + + + search_field + + + + + + + + + + Qt::Horizontal + + + + 20 + 10 + + + + + + + &Search for: @@ -360,7 +393,20 @@ Future conversion of these books will use the default settings. - + + + + + + + Case sensitive + + + true + + + + &Replace with: @@ -370,29 +416,93 @@ Future conversion of these books will use the default settings. - - - - - - - + - - + + + + + + Apply function after replace: + + + replace_func + + + + + + + + + + Qt::Horizontal + + + + 20 + 10 + + + + + + + + - Apply function &after replace: + &Destination field: - replace_func + destination_field - - - + + + + + + + + Mode: + + + replace_mode + + + + + + + + + + use comma + + + true + + + + + + + Qt::Horizontal + + + + 20 + 10 + + + + + + + Test &text @@ -402,8 +512,8 @@ Future conversion of these books will use the default settings. - - + + Test re&sult @@ -412,17 +522,17 @@ Future conversion of these books will use the default settings. - + Your test: - + - + diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index f5a474edbc..c1ada94a84 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -464,11 +464,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # change case don't cause any changes to the directories in the file # system. This can lead to having the directory names not match the # title/author, which leads to trouble when libraries are copied to - # a case-sensitive system. The following code fixes this by checking - # each segment. If they are different because of case, then rename - # the segment to some temp file name, then rename it back to the - # correct name. Note that the code above correctly handles files in - # the directories, so no need to do them here. + # a case-sensitive system. The following code attempts to fix this + # by checking each segment. If they are different because of case, + # then rename the segment to some temp file name, then rename it + # back to the correct name. Note that the code above correctly + # handles files in the directories, so no need to do them here. for oldseg, newseg in zip(c1, c2): if oldseg.lower() == newseg.lower() and oldseg != newseg: while True: @@ -476,8 +476,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): tempname = os.path.join(curpath, 'TEMP.%f'%time.time()) if not os.path.exists(tempname): break - os.rename(os.path.join(curpath, oldseg), tempname) - os.rename(tempname, os.path.join(curpath, newseg)) + try: + os.rename(os.path.join(curpath, oldseg), tempname) + except (IOError, OSError): + # Windows (at least) sometimes refuses to do the rename + # probably because a file such a cover is open in the + # hierarchy. Just go on -- nothing is hurt beyond the + # case of the filesystem not matching the case in + # name stored by calibre + print 'rename of library component failed' + else: + os.rename(tempname, os.path.join(curpath, newseg)) curpath = os.path.join(curpath, newseg) def add_listener(self, listener): @@ -528,10 +537,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): def get_field(self, idx, key, default=None, index_is_id=False): mi = self.get_metadata(idx, index_is_id=index_is_id, get_cover=True) - try: - return mi[key] - except: - return default + return mi.get(key, default) def standard_field_keys(self): return self.field_metadata.standard_field_keys() diff --git a/src/calibre/utils/pyconsole/__init__.py b/src/calibre/utils/pyconsole/__init__.py new file mode 100644 index 0000000000..a7cb4eed01 --- /dev/null +++ b/src/calibre/utils/pyconsole/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import sys + +from calibre import prints as prints_ + +def prints(*args, **kwargs): + kwargs['file'] = sys.__stdout__ + prints_(*args, **kwargs) + + diff --git a/src/calibre/utils/pyconsole/editor.py b/src/calibre/utils/pyconsole/editor.py new file mode 100644 index 0000000000..68b83539f2 --- /dev/null +++ b/src/calibre/utils/pyconsole/editor.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import sys, textwrap + +from PyQt4.Qt import QTextEdit, Qt, QTextFrameFormat + +from pygments.lexers import PythonLexer, PythonTracebackLexer + +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 + +class EditBlock(object): # {{{ + + def __init__(self, cursor): + self.cursor = cursor + + def __enter__(self): + self.cursor.beginEditBlock() + return self.cursor + + def __exit__(self, *args): + self.cursor.endEditBlock() +# }}} + +class Editor(QTextEdit): + + @property + def doc(self): + return self.document() + + @property + def cursor(self): + return self.textCursor() + + @property + def root_frame(self): + return self.doc.rootFrame() + + @property + def cursor_pos(self): + pass + #pos = self.cursor.position() - self.prompt_frame.firstPosition() + #i = 0 + #for line in self.current_prompt: + # i += self.prompt_len + + def __init__(self, + prompt='>>> ', + continuation='... ', + parent=None): + QTextEdit.__init__(self, parent) + self.buf = '' + self.prompt_frame = None + self.current_prompt = [''] + self.allow_output = False + self.prompt_frame_format = QTextFrameFormat() + self.prompt_frame_format.setBorder(1) + self.prompt_frame_format.setBorderStyle(QTextFrameFormat.BorderStyle_Solid) + self.prompt_len = len(prompt) + + self.doc.setMaximumBlockCount(10000) + self.lexer = PythonLexer(ensurenl=False) + self.tb_lexer = PythonTracebackLexer() + self.formatter = Formatter(prompt, continuation) + + motd = textwrap.dedent('''\ + # Python {0} + # {1} {2} + '''.format(sys.version.splitlines()[0], __appname__, + __version__)) + + with EditBlock(self.cursor): + self.render_block(motd) + + sys.stdout = sys.stderr = DummyFile(parent=self) + sys.stdout.write_output.connect(self.show_output) + self.interpreter = Interpreter(parent=self) + self.interpreter.show_error.connect(self.show_error) + + #it = self.prompt_frame.begin() + #while not it.atEnd(): + # bl = it.currentBlock() + # prints(repr(bl.text())) + # it += 1 + + + # Rendering {{{ + + def render_block(self, text, restore_prompt=True): + self.formatter.render(self.lexer.get_tokens(text), self.cursor) + self.cursor.insertBlock() + self.cursor.movePosition(self.cursor.End) + if restore_prompt: + self.render_current_prompt() + + def clear_current_prompt(self): + if self.prompt_frame is None: + c = self.root_frame.lastCursorPosition() + self.prompt_frame = c.insertFrame(self.prompt_frame_format) + self.setTextCursor(c) + else: + c = self.prompt_frame.firstCursorPosition() + self.setTextCursor(c) + c.setPosition(self.prompt_frame.lastPosition(), c.KeepAnchor) + c.removeSelectedText() + c.setPosition(self.prompt_frame.firstPosition()) + + def render_current_prompt(self): + self.clear_current_prompt() + + for i, line in enumerate(self.current_prompt): + start = i == 0 + end = i == len(self.current_prompt) - 1 + self.formatter.render_prompt(not start, self.cursor) + self.formatter.render(self.lexer.get_tokens(line), self.cursor) + if not end: + self.cursor.insertText('\n') + + def show_error(self, is_syntax_err, tb): + if self.prompt_frame is not None: + # At a prompt, so redirect output + return prints(tb) + try: + self.buf += tb + if is_syntax_err: + self.formatter.render_syntax_error(tb, self.cursor) + else: + self.formatter.render(self.tb_lexer.get_tokens(tb), self.cursor) + except: + prints(tb) + + def show_output(self, raw): + if self.prompt_frame is not None: + # At a prompt, so redirect output + return prints(raw) + try: + self.current_prompt_range = None + self.buf += raw + self.formatter.render_raw(raw, self.cursor) + except: + prints(raw) + + # }}} + + # Keyboard handling {{{ + + 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() + elif text: + self.text_typed(text) + else: + QTextEdit.keyPressEvent(self, ev) + + def left_pressed(self): + pass + + def right_pressed(self): + if self.prompt_frame is not None: + c = self.cursor + c.movePosition(c.NextCharacter) + self.setTextCursor(c) + + def home_pressed(self): + if self.prompt_frame is not None: + c = self.cursor + c.movePosition(c.StartOfLine) + c.movePosition(c.NextCharacter, n=self.prompt_len) + self.setTextCursor(c) + + def end_pressed(self): + if self.prompt_frame is not None: + c = self.cursor + c.movePosition(c.EndOfLine) + self.setTextCursor(c) + + def enter_pressed(self): + if self.prompt_frame is None: + return + if self.current_prompt[0]: + c = self.root_frame.lastCursorPosition() + self.setTextCursor(c) + old_pf = self.prompt_frame + self.prompt_frame = None + oldbuf = self.buf + self.buf = '' + ret = self.interpreter.runsource('\n'.join(self.current_prompt)) + if ret: # Incomplete command + self.buf = oldbuf + self.prompt_frame = old_pf + self.current_prompt.append('') + else: # Command completed + self.current_prompt = [''] + old_pf.setFrameFormat(QTextFrameFormat()) + self.render_current_prompt() + + def text_typed(self, text): + if not self.current_prompt[0]: + self.cursor.beginEditBlock() + else: + self.cursor.joinPreviousEditBlock() + self.current_prompt[-1] += text + self.render_current_prompt() + self.cursor.endEditBlock() + + + # }}} + + diff --git a/src/calibre/utils/pyconsole/formatter.py b/src/calibre/utils/pyconsole/formatter.py new file mode 100644 index 0000000000..7f99983ef6 --- /dev/null +++ b/src/calibre/utils/pyconsole/formatter.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import QTextCharFormat, QFont, QBrush, QColor + +from pygments.formatter import Formatter as PF +from pygments.token import Token + +class Formatter(object): + + def __init__(self, prompt, continuation, **options): + if len(prompt) != len(continuation): + raise ValueError('%r does not have the same length as %r' % + (prompt, continuation)) + + self.prompt, self.continuation = prompt, continuation + + pf = PF(**options) + self.styles = {} + self.normal = self.base_fmt() + 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 ndef['bold']: + fmt.setFontWeight(QFont.Bold) + if ndef['italic']: + fmt.setFontItalic(True) + if ndef['underline']: + fmt.setFontUnderline(True) + if ndef['bgcolor']: + fmt.setBackground(QBrush(QColor('#%s'%ndef['bgcolor']))) + if ndef['border']: + pass # No support for borders + + self.styles[ttype] = fmt + + def base_fmt(self): + fmt = QTextCharFormat() + fmt.setFontFamily('monospace') + return fmt + + def render_raw(self, raw, cursor): + cursor.insertText(raw, self.normal) + + def render_syntax_error(self, tb, cursor): + fmt = self.styles[Token.Error] + cursor.insertText(tb, fmt) + + def render(self, tokens, cursor): + lastval = '' + lasttype = None + + for ttype, value in tokens: + while ttype not in self.styles: + ttype = ttype.parent + if ttype == lasttype: + lastval += value + else: + if lastval: + fmt = self.styles[lasttype] + cursor.insertText(lastval, fmt) + lastval = value + lasttype = ttype + + if lastval: + fmt = self.styles[lasttype] + cursor.insertText(lastval, fmt) + + def render_prompt(self, is_continuation, cursor): + pr = self.continuation if is_continuation else self.prompt + fmt = self.styles[Token.Generic.Subheading] + cursor.insertText(pr, fmt) + + diff --git a/src/calibre/utils/pyconsole/main.py b/src/calibre/utils/pyconsole/main.py new file mode 100644 index 0000000000..c2694aae5f --- /dev/null +++ b/src/calibre/utils/pyconsole/main.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' +__version__ = '0.1.0' + +from PyQt4.Qt import QMainWindow, QToolBar, QStatusBar, QLabel, QFont, Qt, \ + QApplication + +from calibre.constants import __appname__, __version__ +from calibre.utils.pyconsole.editor import Editor + +class MainWindow(QMainWindow): + + def __init__(self, default_status_msg): + + QMainWindow.__init__(self) + + self.resize(600, 700) + + # Setup status bar {{{ + self.status_bar = QStatusBar(self) + self.status_bar.defmsg = QLabel(__appname__ + _(' console ') + + __version__) + self.status_bar._font = QFont() + 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 = Editor(parent=self) + self.setCentralWidget(self.editor) + + + +def main(): + QApplication.setApplicationName(__appname__+' console') + QApplication.setOrganizationName('Kovid Goyal') + app = QApplication([]) + m = MainWindow(_('Welcome to') + ' ' + __appname__+' console') + m.show() + app.exec_() + + +if __name__ == '__main__': + main() + diff --git a/src/calibre/utils/pyconsole/repl.py b/src/calibre/utils/pyconsole/repl.py new file mode 100644 index 0000000000..de6262de14 --- /dev/null +++ b/src/calibre/utils/pyconsole/repl.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from code import InteractiveInterpreter + +from PyQt4.Qt import QObject, pyqtSignal + +from calibre import isbytestring +from calibre.constants import preferred_encoding + +class Interpreter(QObject, InteractiveInterpreter): + + # show_error(is_syntax_error, traceback) + show_error = pyqtSignal(object, object) + + def __init__(self, local={}, parent=None): + QObject.__init__(self, parent) + if '__name__' not in local: + local['__name__'] = '__console__' + if '__doc__' not in local: + local['__doc__'] = None + InteractiveInterpreter.__init__(self, locals=local) + + def showtraceback(self, *args, **kwargs): + self.is_syntax_error = False + InteractiveInterpreter.showtraceback(self, *args, **kwargs) + + def showsyntaxerror(self, *args, **kwargs): + self.is_syntax_error = True + InteractiveInterpreter.showsyntaxerror(self, *args, **kwargs) + + def write(self, tb): + self.show_error.emit(self.is_syntax_error, tb) + +class DummyFile(QObject): + + # write_output(unicode_object) + write_output = pyqtSignal(object) + + def __init__(self, parent=None): + QObject.__init__(self, parent) + self.closed = False + self.name = 'console' + self.softspace = 0 + + def flush(self): + pass + + def close(self): + pass + + def write(self, raw): + #import sys, traceback + #print >> sys.__stdout__, 'file,write stack:\n', ''.join(traceback.format_stack()) + if isbytestring(raw): + try: + raw = raw.decode(preferred_encoding, 'replace') + except: + raw = repr(raw) + if isbytestring(raw): + raw = raw.decode(preferred_encoding, 'replace') + self.write_output.emit(raw) +