From 4f1295eb724222fcbed349503ffb36309c72a45e Mon Sep 17 00:00:00 2001 From: Vincent Ullmann Date: Thu, 15 Jan 2026 17:55:45 +0000 Subject: [PATCH 1/8] add PythonSyntaxHighlighter to the PythonCodeEditor Widget --- .../tools/console_interpreter/ui/widgets.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/client/ayon_core/tools/console_interpreter/ui/widgets.py b/client/ayon_core/tools/console_interpreter/ui/widgets.py index 3dc55b081ca..858341bf8ac 100644 --- a/client/ayon_core/tools/console_interpreter/ui/widgets.py +++ b/client/ayon_core/tools/console_interpreter/ui/widgets.py @@ -1,6 +1,82 @@ +from __future__ import annotations + from code import InteractiveInterpreter +import os from qtpy import QtCore, QtWidgets, QtGui +import pygments.lexers +import pygments.styles +import pygments.token + + +def style_to_formats( + 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 + + +class PythonSyntaxHighlighter(QtGui.QSyntaxHighlighter): + def __init__(self, document, style_name: str = ""): + super().__init__(document) + self.lexer = pygments.lexers.PythonLexer() + + style_name = os.getenv("AYON_CONSOLE_INTERPRETER_STYLE", "github-dark") + self.style = pygments.styles.get_style_by_name(style_name) + if not self.style: + all_styles = ", ".join(pygments.styles.get_all_styles()) + msg = f"'{style_name}' not found. Installed styles: {all_styles}" + raise ValueError(msg) + + self.formats = style_to_formats(self.style) + + def highlightBlock(self, text: str) -> None: + index = 0 + for token, value in self.lexer.get_tokens(text): + length = len(value) + + fmt = self.formats.get(token) + if fmt: + 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 class PythonCodeEditor(QtWidgets.QPlainTextEdit): @@ -12,6 +88,7 @@ def __init__(self, parent): self.setObjectName("PythonCodeEditor") self._indent = 4 + _ = PythonSyntaxHighlighter(self.document()) def _tab_shift_right(self): cursor = self.textCursor() From 1fc224036022a5350d91f0be6add7dcc9c740b85 Mon Sep 17 00:00:00 2001 From: Vincent Ullmann Date: Thu, 15 Jan 2026 18:15:09 +0000 Subject: [PATCH 2/8] add background color from theme --- client/ayon_core/tools/console_interpreter/ui/widgets.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/console_interpreter/ui/widgets.py b/client/ayon_core/tools/console_interpreter/ui/widgets.py index 858341bf8ac..0e69e46e332 100644 --- a/client/ayon_core/tools/console_interpreter/ui/widgets.py +++ b/client/ayon_core/tools/console_interpreter/ui/widgets.py @@ -88,7 +88,12 @@ def __init__(self, parent): self.setObjectName("PythonCodeEditor") self._indent = 4 - _ = PythonSyntaxHighlighter(self.document()) + self._highlighter = PythonSyntaxHighlighter(self.document()) + + if self._highlighter.style: + if background_color := self._highlighter.style.background_color: + self.setStyleSheet(f"QPlainTextEdit {{background-color: {background_color};}}") + def _tab_shift_right(self): cursor = self.textCursor() From 05e2864db47a69f7ae36552693acb96239d31430 Mon Sep 17 00:00:00 2001 From: Vincent Ullmann Date: Thu, 15 Jan 2026 18:24:47 +0000 Subject: [PATCH 3/8] fix linter errors --- client/ayon_core/tools/console_interpreter/ui/widgets.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/console_interpreter/ui/widgets.py b/client/ayon_core/tools/console_interpreter/ui/widgets.py index 0e69e46e332..9f5068df10f 100644 --- a/client/ayon_core/tools/console_interpreter/ui/widgets.py +++ b/client/ayon_core/tools/console_interpreter/ui/widgets.py @@ -92,8 +92,11 @@ def __init__(self, parent): if self._highlighter.style: if background_color := self._highlighter.style.background_color: - self.setStyleSheet(f"QPlainTextEdit {{background-color: {background_color};}}") - + self.setStyleSheet(( + "QPlainTextEdit {" + f"background-color: {background_color};" + "}" + )) def _tab_shift_right(self): cursor = self.textCursor() From 58e5500cb1807f6e0decacb7555f203d5e916403 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Jan 2026 09:40:13 +0100 Subject: [PATCH 4/8] Apply dedicated AYON code style (matching Github dark style closely), apply no background color Also remove walrus operator for backwards compatibility --- .../tools/console_interpreter/code_style.py | 134 ++++++++++++++++++ .../tools/console_interpreter/ui/widgets.py | 25 ++-- 2 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 client/ayon_core/tools/console_interpreter/code_style.py diff --git a/client/ayon_core/tools/console_interpreter/code_style.py b/client/ayon_core/tools/console_interpreter/code_style.py new file mode 100644 index 00000000000..d5d12c63091 --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/code_style.py @@ -0,0 +1,134 @@ +from pygments.token import * +from pygments.style import Style + +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 AYONStyle(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/widgets.py b/client/ayon_core/tools/console_interpreter/ui/widgets.py index 9f5068df10f..716765fb3dc 100644 --- a/client/ayon_core/tools/console_interpreter/ui/widgets.py +++ b/client/ayon_core/tools/console_interpreter/ui/widgets.py @@ -8,6 +8,8 @@ import pygments.styles import pygments.token +from ..code_style import AYONStyle + def style_to_formats( style: pygments.styles.Style @@ -48,13 +50,19 @@ class PythonSyntaxHighlighter(QtGui.QSyntaxHighlighter): def __init__(self, document, style_name: str = ""): super().__init__(document) self.lexer = pygments.lexers.PythonLexer() - - style_name = os.getenv("AYON_CONSOLE_INTERPRETER_STYLE", "github-dark") - self.style = pygments.styles.get_style_by_name(style_name) - if not self.style: - all_styles = ", ".join(pygments.styles.get_all_styles()) - msg = f"'{style_name}' not found. Installed styles: {all_styles}" - raise ValueError(msg) + self.style = AYONStyle + + # Allow to override style by environment variable + override_style_name = os.getenv("AYON_CONSOLE_INTERPRETER_STYLE") + if override_style_name: + style = pygments.styles.get_style_by_name(override_style_name) + if not style: + all_styles = ", ".join(pygments.styles.get_all_styles()) + raise ValueError( + f"'{override_style_name}' not found. " + f"Installed styles: {all_styles}" + ) + self.style = style self.formats = style_to_formats(self.style) @@ -91,7 +99,8 @@ def __init__(self, parent): self._highlighter = PythonSyntaxHighlighter(self.document()) if self._highlighter.style: - if background_color := self._highlighter.style.background_color: + background_color = self._highlighter.style.background_color + if background_color: self.setStyleSheet(( "QPlainTextEdit {" f"background-color: {background_color};" From a51d9b353a78816a47370338db93329a4d32f03e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Jan 2026 09:46:06 +0100 Subject: [PATCH 5/8] Make imports explicit --- .../tools/console_interpreter/code_style.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/console_interpreter/code_style.py b/client/ayon_core/tools/console_interpreter/code_style.py index d5d12c63091..61e67bb5485 100644 --- a/client/ayon_core/tools/console_interpreter/code_style.py +++ b/client/ayon_core/tools/console_interpreter/code_style.py @@ -1,5 +1,20 @@ -from pygments.token import * 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", From bc1f0ce6ec668b4dc9fd7992795e7d37fc510695 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 16 Jan 2026 10:04:27 +0100 Subject: [PATCH 6/8] Move `AYONStyle` -> `AYONCodeStyle` into ui folder --- .../tools/console_interpreter/{ => ui}/code_style.py | 2 +- client/ayon_core/tools/console_interpreter/ui/widgets.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename client/ayon_core/tools/console_interpreter/{ => ui}/code_style.py (99%) diff --git a/client/ayon_core/tools/console_interpreter/code_style.py b/client/ayon_core/tools/console_interpreter/ui/code_style.py similarity index 99% rename from client/ayon_core/tools/console_interpreter/code_style.py rename to client/ayon_core/tools/console_interpreter/ui/code_style.py index 61e67bb5485..c8e034a644a 100644 --- a/client/ayon_core/tools/console_interpreter/code_style.py +++ b/client/ayon_core/tools/console_interpreter/ui/code_style.py @@ -50,7 +50,7 @@ } -class AYONStyle(Style): +class AYONCodeStyle(Style): """A Pygments style similar to GitHub's pretty lights dark theme""" background_color = None diff --git a/client/ayon_core/tools/console_interpreter/ui/widgets.py b/client/ayon_core/tools/console_interpreter/ui/widgets.py index 716765fb3dc..d7d408fe2a3 100644 --- a/client/ayon_core/tools/console_interpreter/ui/widgets.py +++ b/client/ayon_core/tools/console_interpreter/ui/widgets.py @@ -8,7 +8,7 @@ import pygments.styles import pygments.token -from ..code_style import AYONStyle +from .code_style import AYONCodeStyle def style_to_formats( @@ -50,7 +50,7 @@ class PythonSyntaxHighlighter(QtGui.QSyntaxHighlighter): def __init__(self, document, style_name: str = ""): super().__init__(document) self.lexer = pygments.lexers.PythonLexer() - self.style = AYONStyle + self.style = AYONCodeStyle # Allow to override style by environment variable override_style_name = os.getenv("AYON_CONSOLE_INTERPRETER_STYLE") From 7337e962fc543e755ea8ccac4948b02b90cebdec Mon Sep 17 00:00:00 2001 From: Vincent Ullmann Date: Fri, 16 Jan 2026 11:22:03 +0000 Subject: [PATCH 7/8] move PythonSyntaxHighlighter to its own class and safe import it. Improve Error handling for invalid style names --- .../ui/syntax_highlight.py | 93 ++++++++++++++ .../tools/console_interpreter/ui/widgets.py | 115 ++++-------------- 2 files changed, 116 insertions(+), 92 deletions(-) create mode 100644 client/ayon_core/tools/console_interpreter/ui/syntax_highlight.py 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 d7d408fe2a3..f1cee211d50 100644 --- a/client/ayon_core/tools/console_interpreter/ui/widgets.py +++ b/client/ayon_core/tools/console_interpreter/ui/widgets.py @@ -1,90 +1,12 @@ from __future__ import annotations from code import InteractiveInterpreter -import os - from qtpy import QtCore, QtWidgets, QtGui -import pygments.lexers -import pygments.styles -import pygments.token - -from .code_style import AYONCodeStyle - - -def style_to_formats( - 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 - - -class PythonSyntaxHighlighter(QtGui.QSyntaxHighlighter): - def __init__(self, document, style_name: str = ""): - super().__init__(document) - 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: - style = pygments.styles.get_style_by_name(override_style_name) - if not style: - all_styles = ", ".join(pygments.styles.get_all_styles()) - raise ValueError( - f"'{override_style_name}' not found. " - f"Installed styles: {all_styles}" - ) - self.style = style - - self.formats = style_to_formats(self.style) - - def highlightBlock(self, text: str) -> None: - index = 0 - for token, value in self.lexer.get_tokens(text): - length = len(value) - - fmt = self.formats.get(token) - if fmt: - 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 +try: + from .syntax_highlight import PythonSyntaxHighlighter +except ImportError: + PythonSyntaxHighlighter = None # type: ignore class PythonCodeEditor(QtWidgets.QPlainTextEdit): @@ -96,16 +18,25 @@ def __init__(self, parent): self.setObjectName("PythonCodeEditor") self._indent = 4 - self._highlighter = PythonSyntaxHighlighter(self.document()) - - if self._highlighter.style: - background_color = self._highlighter.style.background_color - if background_color: - self.setStyleSheet(( - "QPlainTextEdit {" - f"background-color: {background_color};" - "}" - )) + 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() From e48d7b4a139436e1bdc0f834465454d32df60974 Mon Sep 17 00:00:00 2001 From: Vincent Ullmann Date: Fri, 16 Jan 2026 11:22:33 +0000 Subject: [PATCH 8/8] add pygments to client dependencies --- client/pyproject.toml | 1 + 1 file changed, 1 insertion(+) 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"