diff --git a/rpcs3/rpcs3qt/log_frame.cpp b/rpcs3/rpcs3qt/log_frame.cpp index bbc6fe6145..7689aa8f9c 100644 --- a/rpcs3/rpcs3qt/log_frame.cpp +++ b/rpcs3/rpcs3qt/log_frame.cpp @@ -3,6 +3,7 @@ #include "gui_settings.h" #include "hex_validator.h" #include "memory_viewer_panel.h" +#include "syntax_highlighter.h" #include "Utilities/lockless.h" #include "util/asm.hpp" @@ -131,6 +132,8 @@ log_frame::log_frame(std::shared_ptr _gui_settings, QWidget* paren m_tty->setContextMenuPolicy(Qt::CustomContextMenu); m_tty->document()->setMaximumBlockCount(max_block_count_tty); m_tty->installEventFilter(this); + + m_tty_ansi_highlighter = new AnsiHighlighter(m_tty->document()); m_tty_input = new QLineEdit(); if (m_tty_channel >= 0) @@ -599,8 +602,15 @@ void log_frame::UpdateUI() buf_line.assign(std::string_view(m_tty_buf).substr(str_index, m_tty_buf.find_first_of('\n', str_index) - str_index)); str_index += buf_line.size() + 1; - // Ignore control characters and greater/equal to 0x80 - buf_line.erase(std::remove_if(buf_line.begin(), buf_line.end(), [](s8 c) { return c <= 0x8 || c == 0x7F || (c >= 0xE && c <= 0x1F); }), buf_line.end()); + // Ignore control characters and greater/equal to 0x80, but preserve ESC (0x1B) if ANSI mode is enabled + buf_line.erase(std::remove_if(buf_line.begin(), buf_line.end(), [this](s8 c) { + if (m_ansi_tty) + { + // Keep ESC (0x1B) so ANSI sequences remain intact + return c <= 0x8 || c == 0x7F || (c >= 0xE && c <= 0x1F && c != 0x1B); + } + return c <= 0x8 || c == 0x7F || (c >= 0xE && c <= 0x1F); + }), buf_line.end()); // save old scroll bar state QScrollBar* sb = m_tty->verticalScrollBar(); diff --git a/rpcs3/rpcs3qt/log_frame.h b/rpcs3/rpcs3qt/log_frame.h index 0de081863c..f8dd57605f 100644 --- a/rpcs3/rpcs3qt/log_frame.h +++ b/rpcs3/rpcs3qt/log_frame.h @@ -5,6 +5,7 @@ #include "custom_dock_widget.h" #include "find_dialog.h" +#include "syntax_highlighter.h" #include @@ -69,6 +70,7 @@ private: QPlainTextEdit* m_tty = nullptr; QLineEdit* m_tty_input = nullptr; int m_tty_channel = -1; + AnsiHighlighter* m_tty_ansi_highlighter = nullptr; QAction* m_clear_act = nullptr; QAction* m_clear_tty_act = nullptr; diff --git a/rpcs3/rpcs3qt/log_viewer.h b/rpcs3/rpcs3qt/log_viewer.h index 85ece2688b..8f9e14f833 100644 --- a/rpcs3/rpcs3qt/log_viewer.h +++ b/rpcs3/rpcs3qt/log_viewer.h @@ -9,6 +9,7 @@ #include class LogHighlighter; +class AnsiHighlighter; class gui_settings; class log_viewer : public QWidget @@ -33,6 +34,7 @@ private: QString m_full_log; QPlainTextEdit* m_log_text; LogHighlighter* m_log_highlighter; + AnsiHighlighter* m_ansi_highlighter; std::unique_ptr m_find_dialog; std::bitset<32> m_log_levels = std::bitset<32>(0b11111111u); bool m_show_timestamps = true; diff --git a/rpcs3/rpcs3qt/syntax_highlighter.cpp b/rpcs3/rpcs3qt/syntax_highlighter.cpp index 33d49e678d..0cc16bc30e 100644 --- a/rpcs3/rpcs3qt/syntax_highlighter.cpp +++ b/rpcs3/rpcs3qt/syntax_highlighter.cpp @@ -183,3 +183,104 @@ GlslHighlighter::GlslHighlighter(QTextDocument* parent) : Highlighter(parent) commentStartExpression = QRegularExpression("/\\*"); commentEndExpression = QRegularExpression("\\*/"); } + +AnsiHighlighter::AnsiHighlighter(QTextDocument* parent) : Highlighter(parent) +{ +} + +void AnsiHighlighter::highlightBlock(const QString &text) +{ + // Match ANSI SGR sequences, e.g. "\x1b[31m" or "\x1b[1;32m" + const QRegularExpression ansi_re("\x1b\\[[0-9;]*m"); + const QRegularExpression param_re("\x1b\\[([0-9;]*)m"); + + QTextCharFormat escapeFormat; + escapeFormat.setForeground(Qt::darkGray); + escapeFormat.setFontItalic(true); + + QTextCharFormat currentFormat; + currentFormat.setForeground(gui::utils::get_foreground_color()); + + int pos = 0; + auto it = ansi_re.globalMatch(text); + while (it.hasNext()) + { + auto match = it.next(); + int start = match.capturedStart(); + int length = match.capturedLength(); + + // Apply current format to the chunk before this escape sequence + if (start > pos) + { + setFormat(pos, start - pos, currentFormat); + } + + // Highlight the escape sequence itself + setFormat(start, length, escapeFormat); + + // Parse SGR parameters and update currentFormat + QRegularExpressionMatch pm = param_re.match(match.captured()); + if (pm.hasMatch()) + { + QString params = pm.captured(1); + if (params.isEmpty()) + { + // empty or just \x1b[m = reset + currentFormat = QTextCharFormat(); + currentFormat.setForeground(gui::utils::get_foreground_color()); + } + else + { + const QStringList codes = params.split(';', Qt::SkipEmptyParts); + for (const QString& c : codes) + { + bool ok = false; + const int code = c.toInt(&ok); + if (!ok) continue; + switch (code) + { + case 0: + currentFormat = QTextCharFormat(); + currentFormat.setForeground(gui::utils::get_foreground_color()); + break; + case 1: + currentFormat.setFontWeight(QFont::Bold); + break; + case 3: + currentFormat.setFontItalic(true); + break; + case 4: + currentFormat.setFontUnderline(true); + break; + case 30: currentFormat.setForeground(Qt::black); break; + case 31: currentFormat.setForeground(Qt::red); break; + case 32: currentFormat.setForeground(Qt::darkGreen); break; + case 33: currentFormat.setForeground(Qt::darkYellow); break; + case 34: currentFormat.setForeground(Qt::darkBlue); break; + case 35: currentFormat.setForeground(Qt::darkMagenta); break; + case 36: currentFormat.setForeground(Qt::darkCyan); break; + case 37: currentFormat.setForeground(Qt::lightGray); break; + case 39: currentFormat.setForeground(gui::utils::get_foreground_color()); break; + case 90: currentFormat.setForeground(Qt::darkGray); break; + case 91: currentFormat.setForeground(Qt::red); break; + case 92: currentFormat.setForeground(Qt::green); break; + case 93: currentFormat.setForeground(Qt::yellow); break; + case 94: currentFormat.setForeground(Qt::blue); break; + case 95: currentFormat.setForeground(Qt::magenta); break; + case 96: currentFormat.setForeground(Qt::cyan); break; + case 97: currentFormat.setForeground(Qt::white); break; + // Background and extended colors not yet handled + default: + break; + } + } + } + } + + pos = start + length; + } + + // Apply remaining format + if (pos < text.length()) + setFormat(pos, text.length() - pos, currentFormat); +} diff --git a/rpcs3/rpcs3qt/syntax_highlighter.h b/rpcs3/rpcs3qt/syntax_highlighter.h index 8179cd2257..d5ba800e3b 100644 --- a/rpcs3/rpcs3qt/syntax_highlighter.h +++ b/rpcs3/rpcs3qt/syntax_highlighter.h @@ -52,3 +52,14 @@ class GlslHighlighter : public Highlighter public: explicit GlslHighlighter(QTextDocument* parent = nullptr); }; + +class AnsiHighlighter : public Highlighter +{ + Q_OBJECT + +public: + explicit AnsiHighlighter(QTextDocument* parent = nullptr); + +protected: + void highlightBlock(const QString &text) override; +};