# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """Console base class""" import re from qtconsole.styles import dark_color from qtpy.QtCore import Signal from qtpy.QtGui import QColor, QFont, QTextCharFormat, QTextCursor from qtpy.QtWidgets import QApplication from spyder.plugins.editor.widgets.base import TextEditBaseWidget from spyder.plugins.console.utils.ansihandler import ANSIEscapeCodeHandler from spyder.utils.palette import QStylePalette, SpyderPalette MAIN_BG_COLOR = QStylePalette.COLOR_BACKGROUND_1 MAIN_DEFAULT_FG_COLOR = QStylePalette.COLOR_TEXT_1 MAIN_ERROR_FG_COLOR = SpyderPalette.COLOR_ERROR_1 MAIN_TB_FG_COLOR = QStylePalette.COLOR_ACCENT_3 MAIN_PROMPT_FG_COLOR = SpyderPalette.COLOR_SUCCESS_1 def insert_text_to(cursor, text, fmt): """Helper to print text, taking into account backspaces""" while True: index = text.find(chr(8)) # backspace if index == -1: break cursor.insertText(text[:index], fmt) if cursor.positionInBlock() > 0: cursor.deletePreviousChar() text = text[index+1:] cursor.insertText(text, fmt) class QtANSIEscapeCodeHandler(ANSIEscapeCodeHandler): def __init__(self): ANSIEscapeCodeHandler.__init__(self) self.base_format = None self.current_format = None def set_color_scheme(self, foreground_color, background_color): """Set color scheme (foreground and background).""" if dark_color(foreground_color): self.default_foreground_color = 30 else: self.default_foreground_color = 37 if dark_color(background_color): self.default_background_color = 47 else: self.default_background_color = 40 def set_base_format(self, base_format): self.base_format = base_format def get_format(self): return self.current_format def set_style(self): """ Set font style with the following attributes: 'foreground_color', 'background_color', 'italic', 'bold' and 'underline' """ if self.current_format is None: assert self.base_format is not None self.current_format = QTextCharFormat(self.base_format) # Foreground color if self.foreground_color is None: qcolor = self.base_format.foreground() else: cstr = self.ANSI_COLORS[self.foreground_color-30][self.intensity] qcolor = QColor(cstr) self.current_format.setForeground(qcolor) # Background color if self.background_color is None: qcolor = self.base_format.background() else: cstr = self.ANSI_COLORS[self.background_color-40][self.intensity] qcolor = QColor(cstr) self.current_format.setBackground(qcolor) font = self.current_format.font() # Italic if self.italic is None: italic = self.base_format.fontItalic() else: italic = self.italic font.setItalic(italic) # Bold if self.bold is None: bold = self.base_format.font().bold() else: bold = self.bold font.setBold(bold) # Underline if self.underline is None: underline = self.base_format.font().underline() else: underline = self.underline font.setUnderline(underline) self.current_format.setFont(font) def inverse_color(color): color.setHsv(color.hue(), color.saturation(), 255-color.value()) class ConsoleFontStyle(object): def __init__(self, foregroundcolor, backgroundcolor, bold, italic, underline): self.foregroundcolor = foregroundcolor self.backgroundcolor = backgroundcolor self.bold = bold self.italic = italic self.underline = underline self.format = None def apply_style(self, font, is_default): self.format = QTextCharFormat() self.format.setFont(font) foreground = QColor(self.foregroundcolor) self.format.setForeground(foreground) background = QColor(self.backgroundcolor) self.format.setBackground(background) font = self.format.font() font.setBold(self.bold) font.setItalic(self.italic) font.setUnderline(self.underline) self.format.setFont(font) class ConsoleBaseWidget(TextEditBaseWidget): """Console base widget""" BRACE_MATCHING_SCOPE = ('sol', 'eol') COLOR_PATTERN = re.compile(r'\x01?\x1b\[(.*?)m\x02?') # --- Signals # This signal emits an error text, which corresponds to a Python # traceback. sig_exception_occurred = Signal(dict) userListActivated = Signal(int, str) completion_widget_activated = Signal(str) def __init__(self, parent=None): TextEditBaseWidget.__init__(self, parent) # To adjust some things for the internal console self.setObjectName('console') self.setMaximumBlockCount(300) # ANSI escape code handler self.ansi_handler = QtANSIEscapeCodeHandler() # Disable undo/redo (nonsense for a console widget...): self.setUndoRedoEnabled(False) self.userListActivated.connect( lambda user_id, text: self.completion_widget_activated.emit(text)) background_color = MAIN_BG_COLOR default_foreground_color = MAIN_DEFAULT_FG_COLOR error_foreground_color = MAIN_ERROR_FG_COLOR traceback_foreground_color = MAIN_TB_FG_COLOR prompt_foreground_color = MAIN_PROMPT_FG_COLOR self.default_style = ConsoleFontStyle( foregroundcolor=default_foreground_color, backgroundcolor=background_color, bold=False, italic=False, underline=False) self.error_style = ConsoleFontStyle( foregroundcolor=error_foreground_color, backgroundcolor=background_color, bold=False, italic=False, underline=False) self.traceback_link_style = ConsoleFontStyle( foregroundcolor=traceback_foreground_color, backgroundcolor=background_color, bold=True, italic=False, underline=True) self.prompt_style = ConsoleFontStyle( foregroundcolor=prompt_foreground_color, backgroundcolor=background_color, bold=True, italic=False, underline=False) self.font_styles = (self.default_style, self.error_style, self.traceback_link_style, self.prompt_style) self.set_color_scheme(default_foreground_color, background_color) self.setMouseTracking(True) def set_color_scheme(self, foreground_color, background_color): """Set color scheme of the console (foreground and background).""" self.ansi_handler.set_color_scheme(foreground_color, background_color) background_color = QColor(background_color) foreground_color = QColor(foreground_color) self.set_palette(background=background_color, foreground=foreground_color) self.set_pythonshell_font() # ----- Python shell def insert_text(self, text): """Reimplement TextEditBaseWidget method""" # Eventually this maybe should wrap to insert_text_to if # backspace-handling is required self.textCursor().insertText(text, self.default_style.format) def paste(self): """Reimplement Qt method""" if self.has_selected_text(): self.remove_selected_text() self.insert_text(QApplication.clipboard().text()) def append_text_to_shell(self, text, error, prompt): """ Append text to Python shell In a way, this method overrides the method 'insert_text' when text is inserted at the end of the text widget for a Python shell Handles error messages and show blue underlined links Handles ANSI color sequences Handles ANSI FF sequence """ cursor = self.textCursor() cursor.movePosition(QTextCursor.End) if '\r' in text: # replace \r\n with \n text = text.replace('\r\n', '\n') text = text.replace('\r', '\n') while True: index = text.find(chr(12)) if index == -1: break text = text[index+1:] self.clear() if error: is_traceback = False is_warning = False for line in text.splitlines(True): if (line.startswith(' File') and not line.startswith(' File "<')): is_traceback = True is_warning = False # Show error links in blue underlined text cursor.insertText(' ', self.default_style.format) cursor.insertText(line[2:], self.traceback_link_style.format) else: # Detect if line is a warning. if (re.findall('[A-Z].*Warning', line) != [] or 'warnings.warn' in line or 'WARNING' in line): is_warning = True # Show error/warning messages in red cursor.insertText(line, self.error_style.format) # Don't report warnings as internal errors if not is_warning: self.sig_exception_occurred.emit( dict(text=line, is_traceback=is_traceback) ) elif prompt: # Show prompt in green insert_text_to(cursor, text, self.prompt_style.format) else: # Show other outputs in black last_end = 0 for match in self.COLOR_PATTERN.finditer(text): insert_text_to(cursor, text[last_end:match.start()], self.default_style.format) last_end = match.end() try: for code in [int(_c) for _c in match.group(1).split(';')]: self.ansi_handler.set_code(code) except ValueError: pass self.default_style.format = self.ansi_handler.get_format() insert_text_to(cursor, text[last_end:], self.default_style.format) # # Slower alternative: # segments = self.COLOR_PATTERN.split(text) # cursor.insertText(segments.pop(0), self.default_style.format) # if segments: # for ansi_tags, text in zip(segments[::2], segments[1::2]): # for ansi_tag in ansi_tags.split(';'): # self.ansi_handler.set_code(int(ansi_tag)) # self.default_style.format = self.ansi_handler.get_format() # cursor.insertText(text, self.default_style.format) self.set_cursor_position('eof') self.setCurrentCharFormat(self.default_style.format) def set_pythonshell_font(self, font=None): """Python Shell only""" if font is None: font = QFont() for style in self.font_styles: style.apply_style(font=font, is_default=style is self.default_style) self.ansi_handler.set_base_format(self.default_style.format)