diff --git a/client/ayon_core/tools/console_interpreter/ui/code_style.py b/client/ayon_core/tools/console_interpreter/ui/code_style.py new file mode 100644 index 00000000000..c8e034a644a --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/ui/code_style.py @@ -0,0 +1,149 @@ +from pygments.style import Style +from pygments.token import ( + Comment, + Error, + Generic, + Keyword, + Literal, + Name, + Number, + Operator, + Other, + Punctuation, + String, + Text, + Whitespace, +) + + +pl = { + "syntax-comment": "#8b949e", + "syntax-constant": "#79c0ff", + "syntax-entity": "#d2a8ff", + "syntax-storage-modifier-import": "#c9d1d9", + "syntax-entity-tag": "#7ee787", + "syntax-keyword": "#ff7b72", + "syntax-string": "#a5d6ff", + "syntax-variable": "#ffa657", + "syntax-brackethighlighter-unmatched": "#f85149", + "syntax-invalid-illegal-text": "#f0f6fc", + "syntax-invalid-illegal-bg": "#8e1519", + "syntax-carriage-return-text": "#f0f6fc", + "syntax-carriage-return-bg": "#b62324", + "syntax-string-regexp": "#7ee787", + "syntax-markup-list": "#f2cc60", + "syntax-markup-heading": "#1f6feb", + "syntax-markup-italic": "#c9d1d9", + "syntax-markup-bold": "#e6edf3", + "syntax-markup-deleted-text": "#ffdcd7", + "syntax-markup-deleted-bg": "#67060c", + "syntax-markup-inserted-text": "#aff5b4", + "syntax-markup-inserted-bg": "#033a16", + "syntax-markup-changed-text": "#ffdfb6", + "syntax-markup-changed-bg": "#5a1e02", + "syntax-markup-ignored-text": "#c9d1d9", + "syntax-markup-ignored-bg": "#1158c7", + "syntax-meta-diff-range": "#d2a8ff", + "syntax-brackethighlighter-angle": "#8b949e", + "syntax-sublimelinter-gutter-mark": "#484f58", + "syntax-constant-other-reference-link": "#a5d6ff", +} + + +class AYONCodeStyle(Style): + """A Pygments style similar to GitHub's pretty lights dark theme""" + + background_color = None + default_style = '' + + styles = { + Whitespace: "#f0f6fc", + + Comment: pl["syntax-comment"], + Comment.Hashbang: pl["syntax-comment"], + Comment.Multiline: pl["syntax-comment"], + Comment.Preproc: pl["syntax-comment"], + Comment.Single: pl["syntax-comment"], + Comment.Special: pl["syntax-comment"], + + Generic: "#f0f6fc", + Generic.Deleted: "#8b080b", + Generic.Emph: "#f8f8f2 underline", + Generic.Error: "#f8f8f2", + Generic.Heading: "#f8f8f2 bold", + Generic.Inserted: "#f8f8f2 bold", + Generic.Output: "#adaeb6", + Generic.Prompt: "#f8f8f2", + Generic.Strong: "#f8f8f2", + Generic.Subheading: "#f8f8f2 bold", + Generic.Traceback: "#f8f8f2", + Error: "#f8f8f2", + + Keyword: pl["syntax-keyword"], + Keyword.Constant: pl["syntax-constant"], # Ex. None + Keyword.Declaration: pl["syntax-keyword"], + Keyword.Namespace: pl["syntax-keyword"], + Keyword.Pseudo: pl["syntax-entity"], + Keyword.Reserved: pl["syntax-constant"], + Keyword.Type: pl["syntax-constant"], + + Literal: "#f8f8f2", + Literal.Date: "#f8f8f2", + Literal.String.Affix: "#f8f8f2", + Literal.String.Doc: "#f8f8f2", + Literal.String.Double: "#f8f8f2", + Literal.String.Interpol: "#f8f8f2", + Literal.String.Single: "#f8f8f2", + + Name: pl["syntax-markup-bold"], + Name.Variable: pl["syntax-markup-bold"], + Name.Attribute: pl["syntax-markup-bold"], + Name.Builtin.Pseudo: pl["syntax-markup-bold"], # Ex. self + Name.Builtin: pl["syntax-entity"], # Ex. print() + Name.Class: pl["syntax-variable"], + Name.Constant: pl["syntax-constant"], + Name.Decorator: pl["syntax-entity"], + Name.Entity: pl["syntax-entity"], + Name.Exception: pl["syntax-variable"], + Name.Function: pl["syntax-entity"], + Name.Function.Magic: pl["syntax-entity"], + # Name.Label: "#8be9fd italic", + Name.Namespace: pl["syntax-markup-bold"], + Name.Other: pl["syntax-markup-bold"], + # Name.Other: pl["syntax-variable"], + # Name.Tag: "#ff79c6", + Name.Variable.Class: pl["syntax-variable"], + Name.Variable.Global: pl["syntax-variable"], + Name.Variable.Instance: pl["syntax-markup-bold"], + Name.Variable.Magic: pl["syntax-markup-bold"], + + Number: pl["syntax-constant"], + Number.Bin: pl["syntax-constant"], + Number.Float: pl["syntax-constant"], + Number.Hex: pl["syntax-constant"], + Number.Integer: pl["syntax-constant"], + Number.Integer.Long: pl["syntax-constant"], + Number.Oct: pl["syntax-constant"], + Operator: pl["syntax-constant"], + Operator.Word: pl["syntax-constant"], + + # Other: "#f8f8f2", + Other.Constant: pl["syntax-constant"], + Punctuation: "#f8f8f2", + Punctuation.Definition.Comment: pl["syntax-comment"], + String: pl["syntax-string"], + String.Affix: pl["syntax-string"], + String.Backtick: pl["syntax-string"], + String.Char: pl["syntax-string"], + String.Comment: pl["syntax-comment"], + String.Doc: pl["syntax-string"], + String.Double: pl["syntax-string"], + String.Escape: pl["syntax-string"], + String.Heredoc: pl["syntax-string"], + String.Interpol: pl["syntax-string"], + String.Other: pl["syntax-string"], + String.Regex: pl["syntax-string"], + String.Single: pl["syntax-string"], + String.Symbol: pl["syntax-string"], + Text: pl["syntax-markup-bold"], + } diff --git a/client/ayon_core/tools/console_interpreter/ui/syntax_highlight.py b/client/ayon_core/tools/console_interpreter/ui/syntax_highlight.py new file mode 100644 index 00000000000..dfe1fa15fad --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/ui/syntax_highlight.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import os + +from qtpy import QtGui +import pygments.lexers +import pygments.styles +import pygments.token +from pygments.util import ClassNotFound + +from .code_style import AYONCodeStyle + + +class PythonSyntaxHighlighter(QtGui.QSyntaxHighlighter): + def __init__(self, document, style_name: str = ""): + self.lexer = pygments.lexers.PythonLexer() + self.style = AYONCodeStyle + + # Allow to override style by environment variable + override_style_name = os.getenv("AYON_CONSOLE_INTERPRETER_STYLE") + if override_style_name: + + try: + style = pygments.styles.get_style_by_name(override_style_name) + except ClassNotFound: + all_styles = ", ".join(pygments.styles.get_all_styles()) + raise ValueError( + f"'{override_style_name}' not found. " + f"Installed styles: {all_styles}." + ) + else: + self.style = style + + self.formats = self.style_to_formats(self.style) + + # init after validating and loading the style + super().__init__(document) + + def highlightBlock(self, text: str | None) -> None: + if text is None: + return + + index = 0 + for token, value in self.lexer.get_tokens(text): + length = len(value) + if fmt := self.formats.get(token): + self.setFormat(index, length, fmt) + + index += length + + def _format_for_token(self, token): + if token is None: + return None + if token in self.formats: + return self.formats[token] + if token.parent: + return self._format_for_token(token.parent) + return None + + def style_to_formats( + self, + style: pygments.styles.Style + ) -> dict[pygments.token.Token, QtGui.QTextCharFormat]: + """Convert a Pygments style to a dictionary of Qt formats. + + Args: + style: The Pygments style to convert. + + Returns: + token_format = QTextCharFormat for each token in the style. + + """ + formats = {} + for token, _ in style.styles.items(): + + token_style = style.style_for_token(token) + if not token_style: + continue + + token_format = QtGui.QTextCharFormat() + if color := token_style.get("color"): + color = f"#{color}" + token_format.setForeground(QtGui.QColor(color)) + if token_style.get("bold"): + token_format.setFontWeight(QtGui.QFont.Bold) + if token_style.get("italic"): + token_format.setFontItalic(True) + if token_style.get("underline"): + token_format.setFontUnderline(True) + + formats[token] = token_format + + return formats diff --git a/client/ayon_core/tools/console_interpreter/ui/widgets.py b/client/ayon_core/tools/console_interpreter/ui/widgets.py index 3dc55b081ca..f1cee211d50 100644 --- a/client/ayon_core/tools/console_interpreter/ui/widgets.py +++ b/client/ayon_core/tools/console_interpreter/ui/widgets.py @@ -1,7 +1,13 @@ -from code import InteractiveInterpreter +from __future__ import annotations +from code import InteractiveInterpreter from qtpy import QtCore, QtWidgets, QtGui +try: + from .syntax_highlight import PythonSyntaxHighlighter +except ImportError: + PythonSyntaxHighlighter = None # type: ignore + class PythonCodeEditor(QtWidgets.QPlainTextEdit): execute_requested = QtCore.Signal() @@ -12,6 +18,25 @@ def __init__(self, parent): self.setObjectName("PythonCodeEditor") self._indent = 4 + self._apply_syntax_highlighter() + + def _apply_syntax_highlighter(self): + if PythonSyntaxHighlighter is None: + return + + try: + highlighter = PythonSyntaxHighlighter(self.document()) + except ValueError as e: + print(f"Error applying syntax highlighter: {e!s}") + return + + # Apply background color from the style + if background_color := highlighter.style.background_color: + self.setStyleSheet(( + "QPlainTextEdit {" + f"background-color: {background_color};" + "}" + )) def _tab_shift_right(self): cursor = self.textCursor() diff --git a/client/pyproject.toml b/client/pyproject.toml index 5ae71de18bf..327eeb8532a 100644 --- a/client/pyproject.toml +++ b/client/pyproject.toml @@ -7,6 +7,7 @@ markdown = "^3.4.1" clique = "1.6.*" jsonschema = "^2.6.0" pyblish-base = "^1.8.11" +pygments = "^2.18.0" speedcopy = "^2.1" six = "^1.15" qtawesome = "0.7.3"