diff --git a/imgsrc/trim.svg b/imgsrc/trim.svg
new file mode 100644
index 0000000000..8c8810fc66
--- /dev/null
+++ b/imgsrc/trim.svg
@@ -0,0 +1,688 @@
+
+
+
+
diff --git a/resources/images/trim.png b/resources/images/trim.png
new file mode 100644
index 0000000000..3cb93adfa6
Binary files /dev/null and b/resources/images/trim.png differ
diff --git a/resources/recipes/tagesan.recipe b/resources/recipes/tagesan.recipe
new file mode 100644
index 0000000000..8514162598
--- /dev/null
+++ b/resources/recipes/tagesan.recipe
@@ -0,0 +1,45 @@
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class AdvancedUserRecipe1284927619(BasicNewsRecipe):
+ title = u'Tagesanzeiger'
+ publisher = u'Tamedia AG'
+ oldest_article = 2
+ __author__ = 'noxxx'
+ max_articles_per_feed = 100
+ description = 'tagesanzeiger.ch: Nichts verpassen'
+ category = 'News, Politik, Nachrichten, Schweiz, Zürich'
+ language = 'de'
+
+ conversion_options = {
+ 'comments' : description
+ ,'tags' : category
+ ,'language' : language
+ ,'publisher' : publisher
+ }
+
+ remove_tags = [
+ dict(name='img')
+ ,dict(name='div',attrs={'class':['swissquote ad','boxNews','centerAD','contentTabs2','sbsLabel']})
+ ,dict(name='div',attrs={'id':['colRightAd','singleRight','singleSmallRight','MailInfo','metaLine','sidebarSky','contentFooter','commentInfo','commentInfo2','commentInfo3','footerBottom','clear','boxExclusiv','singleLogo','navSearch','headerLogin','headerBottomRight','horizontalNavigation','subnavigation','googleAdSense','footerAd','contentbox','articleGalleryNav']})
+ ,dict(name='form',attrs={'id':['articleMailForm','commentform']})
+ ,dict(name='div',attrs={'style':['position:absolute']})
+ ,dict(name='script',attrs={'type':['text/javascript']})
+ ,dict(name='p',attrs={'class':['schreiben','smallPrint','charCounter','caption']})
+ ]
+ feeds = [
+ (u'Front', u'http://www.tagesanzeiger.ch/rss.html')
+ ,(u'Zürich', u'http://www.tagesanzeiger.ch/zuerich/rss.html')
+ ,(u'Schweiz', u'http://www.tagesanzeiger.ch/schweiz/rss.html')
+ ,(u'Ausland', u'http://www.tagesanzeiger.ch/ausland/rss.html')
+ ,(u'Digital', u'http://www.tagesanzeiger.ch/digital/rss.html')
+ ,(u'Wissen', u'http://www.tagesanzeiger.ch/wissen/rss.html')
+ ,(u'Panorama', u'http://www.tagesanzeiger.ch/panorama/rss.html')
+ ,(u'Wirtschaft', u'http://www.tagesanzeiger.ch/wirtschaft/rss.html')
+ ,(u'Sport', u'http://www.tagesanzeiger.ch/sport/rss.html')
+ ,(u'Kultur', u'http://www.tagesanzeiger.ch/kultur/rss.html')
+ ,(u'Leben', u'http://www.tagesanzeiger.ch/leben/rss.html')
+ ,(u'Auto', u'http://www.tagesanzeiger.ch/auto/rss.html')]
+
+ def print_version(self, url):
+ return url + '/print.html'
+
diff --git a/resources/recipes/the_marker.recipe b/resources/recipes/the_marker.recipe
new file mode 100644
index 0000000000..e5f1ffc761
--- /dev/null
+++ b/resources/recipes/the_marker.recipe
@@ -0,0 +1,52 @@
+import re
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class AdvancedUserRecipe1283848012(BasicNewsRecipe):
+ description = 'TheMarker Financial News in Hebrew'
+ __author__ = 'TonyTheBookworm, Marbs'
+ cover_url = 'http://static.ispot.co.il/wp-content/upload/2009/09/themarker.jpg'
+ title = u'TheMarker'
+ language = 'he'
+ simultaneous_downloads = 5
+ remove_javascript = True
+ timefmt = '[%a, %d %b, %Y]'
+ oldest_article = 1
+ remove_tags = [dict(name='tr', attrs={'bgcolor':['#738A94']}) ]
+ max_articles_per_feed = 10
+ extra_css='body{direction: rtl;} .article_description{direction: rtl; } a.article{direction: rtl; } .calibre_feed_description{direction: rtl; }'
+ feeds = [(u'Head Lines', u'http://www.themarker.com/tmc/content/xml/rss/hpfeed.xml'),
+ (u'TA Market', u'http://www.themarker.com/tmc/content/xml/rss/sections/marketfeed.xml'),
+ (u'Real Estate', u'http://www.themarker.com/tmc/content/xml/rss/sections/realEstaterfeed.xml'),
+ (u'Wall Street & Global', u'http://www.themarker.com/tmc/content/xml/rss/sections/wallsfeed.xml'),
+ (u'Law', u'http://www.themarker.com/tmc/content/xml/rss/sections/lawfeed.xml'),
+ (u'Media', u'http://www.themarker.com/tmc/content/xml/rss/sections/mediafeed.xml'),
+ (u'Consumer', u'http://www.themarker.com/tmc/content/xml/rss/sections/consumerfeed.xml'),
+ (u'Career', u'http://www.themarker.com/tmc/content/xml/rss/sections/careerfeed.xml'),
+ (u'Car', u'http://www.themarker.com/tmc/content/xml/rss/sections/carfeed.xml'),
+ (u'High Tech', u'http://www.themarker.com/tmc/content/xml/rss/sections/hightechfeed.xml'),
+ (u'Investor Guide', u'http://www.themarker.com/tmc/content/xml/rss/sections/investorGuidefeed.xml')]
+
+ def print_version(self, url):
+ split1 = url.split("=")
+ weblinks = url
+
+ if weblinks is not None:
+ for link in weblinks:
+ #---------------------------------------------------------
+ #here we need some help with some regexpressions
+ #we are trying to find it.themarker.com in a url
+ #-----------------------------------------------------------
+ re1='.*?' # Non-greedy match on filler
+ re2='(it\\.themarker\\.com)' # Fully Qualified Domain Name 1
+ rg = re.compile(re1+re2,re.IGNORECASE|re.DOTALL)
+ m = rg.search(url)
+
+
+ if m:
+ split2 = url.split("article/")
+ print_url = 'http://it.themarker.com/tmit/PrintArticle/' + split2[1]
+
+ else:
+ print_url = 'http://www.themarker.com/ibo/misc/printFriendly.jhtml?ElementId=%2Fibo%2Frepositories%2Fstories%2Fm1_2000%2F' + split1[1]+'.xml'
+
+ return print_url
diff --git a/resources/recipes/wsj.recipe b/resources/recipes/wsj.recipe
index fd5e977d10..88e07bcea3 100644
--- a/resources/recipes/wsj.recipe
+++ b/resources/recipes/wsj.recipe
@@ -70,13 +70,16 @@ class WallStreetJournal(BasicNewsRecipe):
def wsj_add_feed(self,feeds,title,url):
self.log('Found section:', title)
- if url.endswith('whatsnews'):
- articles = self.wsj_find_wn_articles(url)
- else:
- articles = self.wsj_find_articles(url)
+ try:
+ if url.endswith('whatsnews'):
+ articles = self.wsj_find_wn_articles(url)
+ else:
+ articles = self.wsj_find_articles(url)
+ except:
+ articles = []
if articles:
feeds.append((title, articles))
- return feeds
+ return feeds
def parse_index(self):
soup = self.wsj_get_index()
@@ -99,7 +102,7 @@ class WallStreetJournal(BasicNewsRecipe):
url = 'http://online.wsj.com' + a['href']
feeds = self.wsj_add_feed(feeds,title,url)
title = 'What''s News'
- url = url.replace('pageone','whatsnews')
+ url = url.replace('pageone','whatsnews')
feeds = self.wsj_add_feed(feeds,title,url)
else:
title = self.tag_to_string(a)
@@ -141,7 +144,7 @@ class WallStreetJournal(BasicNewsRecipe):
articles = []
flavorarea = soup.find('div', attrs={'class':lambda x: x and 'ahed' in x})
- if flavorarea is not None:
+ if flavorarea is not None:
flavorstory = flavorarea.find('a', href=lambda x: x and x.startswith('/article'))
if flavorstory is not None:
flavorstory['class'] = 'mjLinkItem'
diff --git a/resources/recipes/wsj_free.recipe b/resources/recipes/wsj_free.recipe
index 7f3664f1c4..df8234e8e2 100644
--- a/resources/recipes/wsj_free.recipe
+++ b/resources/recipes/wsj_free.recipe
@@ -54,10 +54,13 @@ class WallStreetJournal(BasicNewsRecipe):
def wsj_add_feed(self,feeds,title,url):
self.log('Found section:', title)
- if url.endswith('whatsnews'):
- articles = self.wsj_find_wn_articles(url)
- else:
- articles = self.wsj_find_articles(url)
+ try:
+ if url.endswith('whatsnews'):
+ articles = self.wsj_find_wn_articles(url)
+ else:
+ articles = self.wsj_find_articles(url)
+ except:
+ articles = []
if articles:
feeds.append((title, articles))
return feeds
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/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py
index c72ad4736f..62507ebfc1 100644
--- a/src/calibre/devices/kobo/driver.py
+++ b/src/calibre/devices/kobo/driver.py
@@ -443,9 +443,9 @@ class KOBO(USBMS):
# Reset Im_Reading list in the database
if oncard == 'carda':
- query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ContentID like \'file:///mnt/sd/%\''
+ query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ReadStatus = 1 and ContentID like \'file:///mnt/sd/%\''
elif oncard != 'carda' and oncard != 'cardb':
- query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ContentID not like \'file:///mnt/sd/%\''
+ query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ReadStatus = 1 and ContentID not like \'file:///mnt/sd/%\''
try:
cursor.execute (query)
diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py
index 8f2550733a..25b6d1aaae 100644
--- a/src/calibre/ebooks/conversion/plumber.py
+++ b/src/calibre/ebooks/conversion/plumber.py
@@ -241,7 +241,7 @@ OptionRecommendation(name='toc_filter',
OptionRecommendation(name='chapter',
recommended_value="//*[((name()='h1' or name()='h2') and "
- r"re:test(., 'chapter|book|section|part\s+', 'i')) or @class "
+ r"re:test(., 'chapter|book|section|part|prologue|epilogue\s+', 'i')) or @class "
"= 'chapter']", level=OptionRecommendation.LOW,
help=_('An XPath expression to detect chapter titles. The default '
'is to consider
or tags that contain the words '
diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui
index b2a3e11b4a..ec5a952346 100644
--- a/src/calibre/gui2/dialogs/metadata_bulk.ui
+++ b/src/calibre/gui2/dialogs/metadata_bulk.ui
@@ -6,8 +6,8 @@
0
0
- 679
- 685
+ 752
+ 715
@@ -410,15 +410,15 @@ Future conversion of these books will use the default settings.
-
-
- Case sensitive
-
-
- true
-
Check this box if the search string must match exactly upper and lower case. Uncheck it if case is to be ignored
+
+ Case sensitive
+
+
+ true
+
-
@@ -511,16 +511,16 @@ field is processed. In regular expression mode, only the matched text is process
-
-
- use comma
-
-
- true
-
If the replace mode is prepend or append, then this box indicates whether a comma or
nothing should be put between the original text and the inserted text
+
+ use comma
+
+
+ true
+
-
diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py
index 26dbda6ca4..53788809b6 100644
--- a/src/calibre/gui2/dialogs/metadata_single.py
+++ b/src/calibre/gui2/dialogs/metadata_single.py
@@ -300,6 +300,24 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.cpixmap = pix
self.cover_data = cdata
+ def trim_cover(self, *args):
+ from calibre.utils.magick import Image
+ cdata = self.cover_data
+ if not cdata:
+ return
+ im = Image()
+ im.load(cdata)
+ im.trim(10)
+ cdata = im.export('jpg')
+ pix = QPixmap()
+ pix.loadFromData(cdata)
+ self.cover.setPixmap(pix)
+ self.cover_changed = True
+ self.cpixmap = pix
+ self.cover_data = cdata
+
+
+
def sync_formats(self):
old_extensions, new_extensions, paths = set(), set(), {}
for row in range(self.formats.count()):
@@ -380,6 +398,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.remove_unused_series)
QObject.connect(self.auto_author_sort, SIGNAL('clicked()'),
self.deduce_author_sort)
+ self.trim_cover_button.clicked.connect(self.trim_cover)
self.connect(self.author_sort, SIGNAL('textChanged(const QString&)'),
self.author_sort_box_changed)
self.connect(self.authors, SIGNAL('editTextChanged(const QString&)'),
diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui
index 74febf9c29..dbf825e706 100644
--- a/src/calibre/gui2/dialogs/metadata_single.ui
+++ b/src/calibre/gui2/dialogs/metadata_single.ui
@@ -625,6 +625,17 @@ Using this button to create author sort will change author sort from red to gree
+ -
+
+
+ Remove border (if any) from cover
+
+
+
+ :/images/trim.png:/images/trim.png
+
+
+
-
diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py
index 647e31ff51..9bc504a001 100644
--- a/src/calibre/gui2/ui.py
+++ b/src/calibre/gui2/ui.py
@@ -19,7 +19,7 @@ from PyQt4.Qt import Qt, SIGNAL, QTimer, \
QMessageBox, QHelpEvent
from calibre import prints
-from calibre.constants import __appname__, isosx
+from calibre.constants import __appname__, isosx, DEBUG
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import prefs, dynamic
from calibre.utils.ipc.server import Server
@@ -533,7 +533,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
# Save the current field_metadata for applications like calibre2opds
# Goes here, because if cf is valid, db is valid.
db.prefs['field_metadata'] = db.field_metadata.all_metadata()
- if db.gm_count > 0:
+ if DEBUG and db.gm_count > 0:
print 'get_metadata cache: {0:d} calls, {1:4.2f}% misses'.format(
db.gm_count, (db.gm_missed*100.0)/db.gm_count)
for action in self.iactions.values():
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)
+